@sema-agent/client-core 0.40.0 → 0.42.0

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +1 -1
  3. package/dist/adapter/downstream/eventToSdkMessage.js +24 -0
  4. package/dist/attachmentsWireCaps.d.ts +43 -8
  5. package/dist/attachmentsWireCaps.js +64 -14
  6. package/dist/classifierVerdictWire.d.ts +0 -25
  7. package/dist/classifierVerdictWire.js +27 -7
  8. package/dist/engineAgentPanelStore.js +102 -13
  9. package/dist/hitl/approvalsFeed.d.ts +2 -2
  10. package/dist/hitl/approvalsFeed.js +57 -2
  11. package/dist/hitl/askGateWire.d.ts +4 -2
  12. package/dist/hitl/askGateWire.js +2 -0
  13. package/dist/hitl/editedRuleTextPrecheck.d.ts +102 -0
  14. package/dist/hitl/editedRuleTextPrecheck.js +91 -0
  15. package/dist/hitl/frameRouter.d.ts +21 -2
  16. package/dist/hitl/frameRouter.js +114 -12
  17. package/dist/hitl/gateLedger.d.ts +27 -0
  18. package/dist/hitl/gateLedger.js +64 -9
  19. package/dist/hitl/hitlBridge.d.ts +70 -14
  20. package/dist/hitl/hitlBridge.js +117 -34
  21. package/dist/hitl/hitlHostSurface.d.ts +33 -3
  22. package/dist/hitl/hitlHostSurface.js +33 -3
  23. package/dist/hitl/parkResolver.js +39 -13
  24. package/dist/hitl/toolApprovalWire.d.ts +133 -3
  25. package/dist/hitl/toolApprovalWire.js +143 -6
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -0
  28. package/dist/model/catalogLoader.js +151 -45
  29. package/dist/printToolResultFrame.d.ts +19 -0
  30. package/dist/seatContract.d.ts +25 -1
  31. package/dist/seatContract.js +29 -2
  32. package/dist/subagent/engineDelegatedPrompt.js +10 -3
  33. package/dist/systemReminderTag.d.ts +52 -0
  34. package/dist/systemReminderTag.js +73 -0
  35. package/docs/INTEGRATION-CLIENTS.md +123 -7
  36. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -28,6 +28,456 @@
28
28
  > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
29
  > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
30
30
 
31
+ ## 0.42.0(2026-08-24)
32
+
33
+ > 🔴 **两阶段协议提醒(发包批的义务,见本档头注)**:本段发布前须由发包批做**阶段一** ——
34
+ > `package.json` bump `0.41.0 → 0.42.0` + 本段标题转日期 + README `Version` 行跟版 +
35
+ > `run-integration-doc-freshness-test.mjs` 的 `FROZEN` 账**追加**一行 `pending: true`(且必须恰是
36
+ > `package.json` 当前版本);发布后**阶段二**把 `releasedAt`(npm gitHead 回读实证)与段 `sha256`
37
+ > 补上并删 `pending`。**施工批不做 bump、不动 `FROZEN`** —— pending 行的豁免押在「尚未发布」上,
38
+ > 而施工批与发布之间还会有别的改动落进本段,提前钉哈希必然自相矛盾。
39
+
40
+ **九件窗([5125]③):#225 编辑臂五条 additive / [5076] precheck 转出口 / A-D2 `resolution` 透传 /
41
+ [4982] 两条 P0-KPI / [C195] 四臂等值断言 / contract §2.4 撤稿 / seatContract 证据等级标注;
42
+ 两件经四源重验后翻案(见文末「翻案与如实登记」)。公开面 **+4 运行期导出**(767 → 771)+ 1 测试钩。**
43
+
44
+ ### 🆕 additive:#225 编辑臂五条([5111] 需求单 / server [5071] `respondFreeFormRules`,server ≥7.44)
45
+
46
+ 上游是 server #340 开出的自由文本规则臂。壳侧 [5111] 探针在 0.41.0 上实测到**闭集三处**——
47
+ ①`persistRule` 是裸 string 无 `edited` 位;②运行期候选表核对自由文本**恒丢键**;③respond 400 原文被
48
+ 编排层折成 `debug` 吞掉(错误反馈面不可达)。五条逐条:
49
+
50
+ 1. **`ApprovalCardAllowDecision.persistRuleEdited?: true`** —— 兄弟位,既有 `persistRule: string`
51
+ **字节不变**。它是**出身声明**不是放行凭据:带上它只让编排层放弃表核,真判官仍在引擎侧。
52
+ 2. **`RespondToolApprovalOpts.persistRuleEdited?: true`** —— 同款兄弟位。🔴 **注入面的映射义务**:
53
+ server 的 respond 体形是 `persistRule: {rule, edited: true}`,本包**刻意保持扁平**(改嵌套形是既有
54
+ 消费者的 BREAKING),由注入面合成。缺席 = 候选臂,`rule_not_offered` 语义一字不变。
55
+ 3. **运行期候选表核在编辑臂在场时让位** —— 判据锚 `card.persistRuleEdited === true`,
56
+ 🔴 **绝不认「文本恰好不在候选表里」**:后者会让一个拼错候选原文的坏卡口自动升级成自由文本臂,
57
+ 把「never sending un-offered text to the rule store」那道核对彻底架空。
58
+ 4. **`ToolApprovalFrameLaneOpts.respondFreeFormRulesCapable?: boolean`** —— 形与判据**逐字照**既有的
59
+ `approvalDecisionNoteCapable`(同一条 SDK 6.16 纪律:**位缺席就别发**)。缺席/false ⇒ 编辑臂的
60
+ `persistRule` **整条不发**,决断照送,老引擎零受迫、行为逐字节 = 0.41.0。
61
+ 5. **`ToolApprovalFrameOutcome.respondRefusal`(+ `readToolApprovalRespondRefusal` 读口)** ——
62
+ respond 抛错时把 server 的响亮拒(400 / `edit-rejected`)**原文交还调用方**。
63
+ 🔴 `decision` 语义**一字未改**(仍是 `'unresolved'`);新位是**附加**诊断面,不是第五种 decision。
64
+ 原样交还零加工(不 trim/不截断/不改写),`status` 缺席时**不折 0**。
65
+ 🔴 **三位全可选、三位皆缺席时整只不铸**(异源复审 [medium] 采纳):`message` 不再无条件退
66
+ `String(err)` —— `throw {}` 铸出来的 `"[object Object]"` 不是 server 说的话,是本层编的。
67
+ 逐位读属性各自 `try`(抛出物可以带 `get message() { throw }`)、`String()` 也包 try
68
+ (`Symbol.toPrimitive` 可以抛):这条收敛路径**永不因不可信抛出物而 reject**。
69
+ 🔴 **日志侧净化**:`errorCode` 交还调用方时**原样**,写进宿主日志时只放行机读码字符集
70
+ (`[A-Za-z0-9._:-]{1,64}`),形不合只报长度 —— 它是 UNTRUSTED 应答体,原样插日志可伪造日志行、
71
+ 可注入终端控制序列。
72
+
73
+ **常驻门**:`run-hitl-gate-honesty-test.mjs` **F12** 三腿(能力位缺席不发 / 表核让位负控 / 400 原文可达),
74
+ 各带负控;`run-client-core-pure-test.mjs` B7 段 16 条。接入契约见 `docs/INTEGRATION-CLIENTS.md` **§4b-2**。
75
+
76
+ ### 🆕 additive:[5076] `precheckEditedRuleText` 转出口(端口注入形,非 re-export)
77
+
78
+ core 5.57.0 导出了与 `confirmRuleApproval` **同一函数体**的 text×command 三步门。新增四个公面符号:
79
+ `EditedRuleTextPrecheck` / `EditedRuleTextPrechecker`(类型面)、`installEditedRuleTextPrechecker` /
80
+ `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`(注入口 + 存在性读口 + 取值读口)。
81
+
82
+ 🔴 **为什么不是 `export { precheckEditedRuleText } from '@sema-agent/core'`**([5076] 自领件的原话):
83
+ value 级 re-export 在本包**做不到**,两条门各自独立否掉它 —— `run-client-core-portability-test.mjs` 的
84
+ `EXPECTED_PACKAGES_INDEX` 是**等值门**(闭包外部包恒等于 `{diff, @sema-agent/sdk}`),而它 ③ 段拿
85
+ esbuild `--platform=browser` **真打一次包**,`@sema-agent/core` 的 barrel 值级拉 `node:crypto`
86
+ (`engine/session/log-digest.js`)、`node:fs`/`node:path`(`core/skills-directory.js`),浏览器腿当场打不成。
87
+ 一条这样的边会把**整台引擎**焊进每一个装本包的端,换来一只纯函数。⇒ 取端口注入形:Node 宿主原样装,
88
+ **浏览器 lane 留缺席走往返形**(登记为 §7 缺口 **P-34**,不是漏装)。
89
+ 🔴 未装时读口返 **`undefined`**(诚实缺席),绝不编一个 `{ok:true}` —— 那是替一只不在场的判官发言。
90
+ 🔴 调用方 bug(`command` 缺席/非串)的响亮拒**原样上抛不吞**:吞掉会把调用方的错渲成对人的判决。
91
+
92
+ ### 🆕 additive:A-D2 —— `tool_end.resolution` 透传(cli wire 锚台账 cli-191 镜头 A)
93
+
94
+ `case 'tool_end'` 的闭形白名单又剥掉了一条**已经在 wire 上**的事实,这是同一病形的**第五例**
95
+ (`toolCallId` [1947] → `governanceForced` [2942] → `settledBy`/`approver` #263 → `_sema_collateral_abort`
96
+ #324 → 本条)。`resolution` = core ≥5.35 `AskDenyResolution` 九词。
97
+
98
+ **消费面已经在等它**:判「审批窗口自己走完了」时单锚 `settledBy` 会把「审批方违约」那一臂一起收进来
99
+ (approver 回了契约外的东西,窗口并没有自然走完)。这一位不过境 ⇒ 判据**缺一臂**,消费端只能退到锚
100
+ core 散文(文案一改就漂)。
101
+
102
+ 🔴 **给消费端的判据是正面匹配**(异源复审 [medium] 采纳,与接入档 §3d-2 同口径):
103
+ `settledBy === 'timeout' && resolution === 'window_expired'`。**绝不**写
104
+ `resolution !== 'approver_contract'` —— 那个不等式对**缺席**(旧引擎不发这一位)与对**任何未来
105
+ 新码**都为真,会把「不知道是怎么拒的」折成「窗口自然结束」。⚠️ **行为收紧披露**:旧引擎(不发
106
+ `resolution`)在正面匹配下**不再可折叠** —— 这是刻意的 fail-closed 方向:没有证据就不声称「窗口
107
+ 走完了」。四条负控钉在 pure 门 A-D2②b-②e。
108
+
109
+ 开集防御读、缺席不铸键、在场原样(与同臂三键同姿势);`ToolEndResultArmLike.resolution` 同批补声明。
110
+ 接入契约见 **§3d-2**。
111
+
112
+ ### 🔴 修复:[4982] 候选① —— `SEMA_ATTACHMENTS=off` 关不掉 `backgroundTasks`(P0-KPI)
113
+
114
+ core **5.12.0(BREAKING)** 把 `attachments.backgroundTasks` 与 `agentListing`/`skillsListing` **同批**
115
+ 翻成 DEFAULT-ON(core `types.d.ts` 逐字:「boolean, not `true`: explicit `false` is the opt-out;
116
+ **same contract as the listing family below**」)。本模块当时**跟修了后两个、漏了第一个** ——
117
+ `backgroundTasks` 一直留在「off = 删键,永不发 false」那一族。后果是这个旋钮上最坏的一种失效:
118
+ `SEMA_ATTACHMENTS=off`(文件自称「真·全关」)对这一位与**什么都不配**逐字节同效(两条路都不发这个键
119
+ = 都是「开」),且**零报错**。
120
+
121
+ **修法 = 把「能不能显式开」与「能不能显式关」拆成两条正交的轴**,`backgroundTasks` 两族兼属:
122
+ - 轴 A(可显式开,`full` 全开的 7 员)—— membership **不动**;
123
+ - 轴 B(可显式关,core DEFAULT-ON)—— `backgroundTasks` **新入**,`off` 与 `-backgroundTasks` 产出 `false`。
124
+
125
+ 🔴 **为什么不把它整个搬进 DEFAULT-ON 族**(那是错的修法,两条理由都是硬的):①`full`/`default`/
126
+ `+backgroundTasks` 三条既有路会当场少发一个 `true` —— 对 core ≥5.12.0 行为等价,但对**更旧引擎**
127
+ 那是真的从「开」变「关」,而本模块手里没有 server 版本这个量;②空 env 的默认产出
128
+ `{backgroundTasks: true, toolsDelta: true}` 是 [487]② CC-parity 的字面承诺。
129
+ ⇒ 本批**只加一条出路**,其余取值**逐字节不变**(三条正控钉住)。
130
+
131
+ **行为变化(端可感,收紧方向)**:`SEMA_ATTACHMENTS=off|0|false|no|none` 现在多发一个
132
+ `backgroundTasks: false`。受损方 = 无(旧行为在**所有** server 代际上都关不掉它)。
133
+
134
+ ### 🔴 修复:[4982] 候选② —— 线上目录「传输 ok、内容被拒」时缓存腿从未被问过(P0-KPI)
135
+
136
+ `loadCatalogWithSources` 的缓存腿门槛是 `hit === null`,而 `hit` 只记录**传输层**结果(200 + 旁签过
137
+ 就算 hit)。于是「线上目录发得下来、但 `schemaVersion` 超区间被 `validateOnlineCatalog` 拒」这条缝里:
138
+ loader 认为「有 hit」⇒ **永不查缓存**(哪怕缓存里躺着一份上次真正被接受过的合法目录),用户只拿到
139
+ 内置精简表;而 `cacheHit:false` 这个**诚实位反过来说谎** —— 读起来是「没有缓存」,真相是「有缓存但
140
+ 从头到尾没被问过」。
141
+
142
+ **修法**:判据锚在**真正决定结果的量**上 —— 决定「用不用得上目录」的是 `base.online.ok`(内容判决),
143
+ 不是 `hit !== null`(传输判决)。缓存腿的读法提成一处(两个调用点不许有两份读法),内容被拒后补一条
144
+ 回落腿。🔴 缓存那份**不开后门**:它照样过同一个 `resolveModelCatalog`;也被拒时**保持第一次的结果**
145
+ (不拿一份同样不合格的文档顶替),`cacheHit` 诚实留在 `false`——本位的语义是「这次的目录**是从缓存
146
+ 来的**」,不是「问过缓存」,抬成 true 会让「缓存救场了」与「缓存也坏了」在读数上不可分。两种情形
147
+ 各有一条**措辞可分**的 warning。
148
+
149
+ ### 🆕 [C195] `classifyTurnWireError` ⇄ web `turn-error-classify` **四臂**等值断言(常驻门)
150
+
151
+ **账目先说清楚**(四源重验):[C195] 说「包内 web 同形样本三臂等值断言已常驻」,C 板 [3903]⑤ 判
152
+ 「查无实物」并让 0.42.0 真补 —— **两句都不完全对**。那三条 `web 等值样本①②③` 自 `6257fe1`
153
+ (`package.json` version = **0.30.3**)起就一直在 `run-client-core-pure-test.mjs` 里,0.36.0 那会儿也在;
154
+ 「查无实物」是**假阴**(测试面不进 `dist`,按 dist 找必然零命中),当时的认账是没核实就跟着认了。
155
+
156
+ **但真缺口确实存在,只是不在那三条上**:它们是**手造近似样本**,不是照 web
157
+ `ui/lib/turn-error-classify.ts` 的**四条真臂**取的;而且那份文件里两处**刻意的**跨端差异一条断言都没有。
158
+ 本批补齐:①http 臂四个状态码逐条;②network↔transport **共用基表 13 个 token 逐 token**对拍
159
+ (抽样对「某条词被谁删掉了」零判别力);③**负控**=浏览器-only 三种说法(`Failed to fetch` /
160
+ `NetworkError` / `Load failed`)在本包判 `internal` —— 宿主词归 web 叠加,包不替它判;
161
+ ④unknown↔internal;⑤**差异登记**=web 的 `timeout` 臂本包无对位,钉住真实落点 + 判别力自证;
162
+ ⑥本包独有的 park 臂登记为超集。
163
+
164
+ 🔴 **异源复审 [medium] 再修一层:token 字符串对拍换成**行为**对拍**。第一版把两边归一成小写、
165
+ 还删掉 `\b` 再比 —— 那样 web 把 `/fetch failed/i` 改成 `/fetch failed/` 时对拍**照绿**,而真实
166
+ 分类行为已经分叉。现在按两边的**真 RegExp**(source + flags 原样重建)在生成样本(原样/全大写/
167
+ 全小写)上逐条跑 `test()` 对判决。同批把容器判据从「那一份文件在不在」换成**仓标志物** ——
168
+ **仓在而分类器被搬走/改名 ⇒ 直接红**(那正是本腿最该报警的场景,第一版会把它当「容器缺席」放过)。
169
+
170
+ 🚩 **对拍当场测出一条真实跨端分叉,如实登记(接入档 §7 缺口 P-35)**:本包 `WIRE_NETWORK_ERROR_PATTERN`
171
+ 整条带 `i`,而 web 的 errno 类 token(`ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `EAI_AGAIN` /
172
+ `ENOTFOUND` / `EHOSTUNREACH` / `ENETUNREACH` / `EPIPE` / `UND_ERR`)**逐条无 `i`** ⇒ 同一条小写 errno
173
+ 文本,本包判 `transport`、web 判 `unknown`。分叉**不抹平**(抹平就是替 web 改它的判定),而是
174
+ 登记 + 用门钉住:未登记的新分歧红、**登记的分叉消失也红**(逼下一棒删登记而不是让它长住)。
175
+
176
+ ### 🔴 撤稿:contract §2.4「DENY-abort」句(server [4833] 明请,契约成文 `4631a0f` 随 7.39 出)
177
+
178
+ server `ASSISTANT-WIRE-CONTRACT.md` **§4a** 逐字:**DENY is a TOOL-level answer, NEVER a run kill**;
179
+ 客户端不得把用户的拒绝译成 cancel;两动词的 wire 判别式 = `cancelled` vs `gate.batch_halted`。
180
+ 包内四处注释引 `contract/04 §2.4` 当权威,其中两条断言已被证伪,逐条撤:
181
+
182
+ - **「deny 用来 cancel 一条 run」** —— §4a 反过来说。四处引用改锚 §4a
183
+ (`hitlBridge.decideTool` 头注 + `canUseTool` deny 臂 / `parkResolver` 中断臂 / `toolApprovalWire`
184
+ aborted 臂 / `askGateWire` 模块头注);
185
+ - **「`runs.cancel` 对 suspended run 回 409」** —— 自 server [868] 起**就地取消**已实装
186
+ (`cancelSuspended` 有实体,409 只剩 `conflict.approval_settled` 一条;本仓
187
+ `docs/fresh-scan-client-core-2026-08-08.md` B型-5 早已按真字节证伪,当时未跟修注释)。
188
+ `parkResolver` 那句「引擎侧解锁腿**到货前**这是壳能做的最诚实半场」同批订正 —— 那条腿早就到货了。
189
+
190
+ **`CANCEL_DENY_BUDGET_MS` 论证前提重审**:2s 预算的原理由(「引擎收到 deny 即终结 run」)**作废**;
191
+ 它换一个理由仍然成立(观察上限,给「decide 永不返回」一条出声的路),但**数值本批不动** ——
192
+ 动它是行为面改动,判据只有真实 decide 往返分布说了算,而那份实测在消费端;且下游 `sema-cli` 的
193
+ `hitlCancelDeny.test.ts` 按现值锁着行为,单边改会当场把消费端打红。残余如实登记为 §7 缺口 **P-33**。
194
+
195
+ **行为零变化**:本件只改散文与注释锚点,`decideTool` 本来走的就是 §4a 说的那条 TOOL 级 decide 通路。
196
+
197
+ ### 🆕 seatContract 证据等级标注(test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件①)
198
+
199
+ `src/seatContract.ts` 里所有以「CC 如何如何」为形的断言 —— `212 methods` / `854-channel census` /
200
+ 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标 —— 加**证据等级标注**:
201
+ **桌面 unpack(CC Electron app 的 `.vite/build` 产物),本地语料库不可复验**。
202
+
203
+ 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,这不是「语料版本旧」而是**产物形态不对**
204
+ —— 拿 CLI 形态的语料去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不能证真也不能证伪**。
205
+ 🔴 标注写在源码里(而不是只写进不进 tarball 的台账),因为**这段注释随 `dist/*.d.ts` 出包**,
206
+ 三端读到的就是它;只写台账 = 消费端仍读到一句看起来可复验、实际不可复验的断言(与本文件
207
+ `workerBadge` 出身订正同一条理由)。**对下游的判据**:复核这些断言**不得**拿本仓 cc-decoded 当反证。
208
+
209
+ ### 🚩 翻案与如实登记(四源重验,不硬做)
210
+
211
+ - **`engine_notice` 投影臂([5069] S13 第二实例)—— 本仓半场早在 0.38.0 就已落地,0.42.0 无施工面**。
212
+ 直证:`016e257`(#318 合批四件)已落 `eventToSdkMessage` raw 预分派 + `adapt` chrome 臂
213
+ `engineNoticeArm` + `INTERNAL_SDK_ARM_TYPES` 登记 + 接入档 **§3f**(键形 + 端的五条消费纪律 + 两条
214
+ 如实登记),常驻门在 `#318 段` 24 条。[5069] 自己的核实动作④也写着「client-core `adapt/arms.ts`
215
+ 确认真实注册在生产投影表里」。⇒ 缺的**只有**壳侧渲染腿(cli `upstreamBridge.consumeChrome` 的
216
+ `default` 静默吞),那是 cli 仓的批面,本仓无对应施工。0.42.0 顺手复验了 §3f 那条「SDK 类型面尚未
217
+ 到货」的登记:`@sema-agent/sdk@7.2.0` 的 `dist/` 全树 `engine_notice` 仍零命中(正控 `cycleSeq` 有命中,
218
+ 证明探针会说话)⇒ 该登记**仍然成立**,raw 预分派与自退休钉原样保留。
219
+ - **[C195] 三臂等值断言「查无实物」= 假阴**,详见上文 [C195] 段的账目说明;本批补的是**四臂真样本 +
220
+ 两处刻意差异的负控**,不是「从无到有」。
221
+
222
+ ### 门与验证
223
+
224
+ - **全门族 32/32 绿**;`run-client-core-pure-test.mjs` 3062 条(B3 226→252 / B7-core 291→330 /
225
+ FIX7 82→89,均**贴当日实测零松量**)、`run-hitl-gate-honesty-test.mjs` 179→196、
226
+ `run-catalog-loader-gates-test.mjs` 118→137、`run-integration-doc-freshness-test.mjs` 464→468、
227
+ `run-engine-vocab-floor-test.mjs` 76→93。
228
+ - **🔴 devDep `@sema-agent/core` `^5.50.0` → `^5.57.0` + `CORE_FLOOR` 5.50 → 5.57**(异源复审
229
+ [high] 采纳):本批的类型面自称「逐形镜像 core 5.57.0 的 `precheckEditedRuleText`」,而对账物
230
+ 停在 5.50(lock 实装 5.52.0,**根本没有那个导出**)⇒「导出不存在 / 返回形漂了」两种真故障在门上
231
+ 一条都看不见,而 Node 宿主照接入档 import 会当场炸。同批加 **G4 段**:直接
232
+ `import('@sema-agent/core')` 拿**真函数**装进本包注入口跑正负例 + 返回形逐键对账 + 调用方 bug
233
+ 响亮抛。**这是恒绿病根的第四次复发**(前三次 5.10→5.20 / 5.20→5.43 / 5.43→5.50,各有记账)。
234
+ - **🔴 地板一抬,G3-a 当场翻红(红先绿后留证)**:core 5.5x 中段把 `capRefusal` helper 拆掉了 ——
235
+ 拒绝对象改在 `admit(kind)` 里就地铸,孪生拼法 `{error, code}` 挪到四个 `errorResult(...)` 调用位。
236
+ 老锚(函数名字面)在 5.57 上抽不出体 ⇒ 门走 exit 9「判据没有判别力」。**重锚不锚函数名**(那正是
237
+ 刚烂掉的东西),改成对「凡交出 `refusal.text` 的 `errorResult` 调用」做**全称**断言 + 两码字面锚
238
+ `code:` 铸点位。这正是本地板存在的理由。
239
+ - **棘轮登记**:portability `MAX_INDEX_CLOSURE_FILES` 136→137(新叶件零 import、
240
+ **不动 `EXPECTED_PACKAGES_INDEX`**)· singleton `HIGH_RISK_CEILING` 92→93(注入槽的装-读分裂属
241
+ `queuePort`/`tailMetaSink` 同族,逐条理由在册)· typeshape `unknownExport` 253→255
242
+ (`ToolEndResultArmLike.resolution` 与 `readToolApprovalRespondRefusal(err: unknown)`,各与同族既有位
243
+ 同理由)。
244
+ - **异源复审第三轮四条全采纳(门本身的假绿口)**:
245
+ · **G4 对拍了另一份 core**([high])—— 裸 `import('@sema-agent/core')` 走本仓 node_modules,而本门
246
+ 前半段的 `CORE` 可来自 `SEMA_CORE_PKG` / 兄弟树 ⇒ 会「拿 A 的版本号报 B 的行为」。改成按选定
247
+ `CORE` 目录的真入口导入 + 实例一致性断言;
248
+ · **[C195] 动态登记掩盖 web 删词**([high])—— 「含大写就算已登记」会让 web **删掉**
249
+ `/ECONNRESET/` 这种真回归自动过关。登记表改**静态审核表** + 补一条独立的**存在性**双向断言
250
+ (shell ⊆ web)+ 登记表僵尸行反钉;变异 M14(模拟 web 删词)实撞 RED;
251
+ · **`errorResult` 括号平衡抽取会吞并调用**([medium])—— 字符串/模板里一个不配对的 `(` 就能把两个
252
+ 真实调用合成一片,前一个缺 `code` 也被后一个命中。换 **TS AST** 逐 `CallExpression` 取,
253
+ 正控样本专门造「字符串里有不配对括号」的形;
254
+ · **refusal 的「在场即至少一位」没进类型**([medium])—— 三位全 optional 允许 `{}`。改成**三选一
255
+ 联合**并在构造侧逐位收窄,编译期与运行期是同一条不变量。
256
+
257
+ - **变异自证 13 条**(cp 备份 + 逐字节复原核对,全部 IDENTICAL):
258
+ M1 表核让位 / M2b 能力位闸反接 / M3 400 原文交还 / M4b `resolution` 恒不铸键 /
259
+ M5 `backgroundTasks` 归族 / M6b 缓存回落腿判据反接 / M7 预检诚实缺席 /
260
+ M8 候选链内容级 failover / M9 归因分类(混合故障)/ M10c `errorCode` 日志净化 /
261
+ M11b refusal 占位串黑名单 / M12 C195 行为对拍(基表掉一词)/ M13b G4 真 core 装口 /
262
+ M14 C195 存在性(模拟 web 删 `/ECONNRESET/`)——
263
+ **各自打点后单跑对应门,全部 RED**,复原后全部 GREEN。
264
+ 🔴 **凡首轮判「编译期红」的一律换成编译得过的等效变异重跑**(编译红只证明动了源码,不证明断言
265
+ 抓得住)。**M10 第一次跑出 GREEN = 真覆盖缺口**:`logSafeErrorCode` 当时没有任何断言看着它,
266
+ 按 [mutation-must-match-guard-class] 补了 F12 腿3b 三条(日志零脏字节 / 交还原样 / 形合码照写)
267
+ 之后重跑才 RED —— 这一格如实记账,不当作「变异没抓到所以没问题」。
268
+
269
+ ## 0.41.0(2026-08-23)
270
+
271
+ **patch:七件真缺陷修(逃逸计数跨腿残留 / 面板缓冲无界 / 单源闸的撞名绕行口 / 决断成功摘错扣留帧 /
272
+ 连坐 abort 帧的判据认不出旁观者 / `<system-reminder>` 带属性后两处判定恒失效)。**
273
+ 各件互不相干,各自红先绿后、各自带常驻回归钉;公开面**零新增导出**(`GatePark.gatedCallId` 与
274
+ `GateLedger.dropHeldForDecidedPark` 都在既有类型上 additive;新叶件 `systemReminderTag.ts` 是包内
275
+ 闸口,不进 barrel,消费者全在包内)。
276
+
277
+ - **🔴 [4974] 件⑥⑦(同根因双座位):core ≥5.46 的 `<system-reminder>` **开标签恒带 `mark`**,
278
+ 两处判定还锚着裸标签**。真字节直证(core 5.52.0 `dist/core/reminder-mint.js`):
279
+ `openSystemReminder(mark)` = 有 mark 时铸 `<system-reminder mark="<22 位 base64url>">`,
280
+ `mintSystemReminder(body, mark)` = 开标签 + `\n` + 正文 + `\n</system-reminder>`(闭标签不变)。
281
+ 两个座位:
282
+ · **件⑥ `classifierVerdictWire.unwrapSystemReminder`** —— 剥不掉壳 ⇒ 裁决原文前顶着整个开标签 ⇒
283
+ `startsWith(签名)` 恒不成立 ⇒ auto-deny 的通知行与 Recent Denials 记账**静默失效**(现网 core
284
+ 全都带 mark,等于这条呈现面整条不在);
285
+ · **件⑦ `subagent/engineDelegatedPrompt.stripEnvelopes`** —— 信封整段剥不掉 ⇒ Fleet 详情页的
286
+ 「Prompt」又变回 ~1330 字符技能清单(该函数头注自陈 2026-08-01 修过的 bug 原样复发)。
287
+ 修:识别口径收进**单一闸口** `src/systemReminderTag.ts`(零 import 叶件)。开标签是**闭集**形
288
+ `<system-reminder(?: mark="[A-Za-z0-9_-]{22}")?>` —— core 只铸这两种(`openSystemReminder` 逐字,
289
+ mark 值形 = `MARK_SHAPE`),所以判据也只认这两种:**不做通配属性**(七审 finding② 采纳)。
290
+ 宽口会把 `onclick="…"` / 换行垃圾 / 未闭合引号 / 内嵌 `<img` 都当成合法开标签,而这两个消费点
291
+ 处理的正是**模型与工具产出的文本**(UNTRUSTED)—— 等于给伪造信封开口子:可以拿假信封把内容藏起来
292
+ (委派 prompt 面)或把自己的文本伪装成引擎裁决(分类器面)。闭标签逐字不放宽,同理。两处判定改经闸口 —— 两处各写一份正则正是这条缺陷能长出
293
+ **两个**座位的原因。`fork-boilerplate` 在 core dist 里是**裸**标签,保持精确形(没有属性的标签
294
+ 不需要属性容忍)。🔴 闸口**刻意不校验 mark 值**:它的职责是「认出这是引擎的信封壳」并剥掉,
295
+ 不是信任判定(core 的 `reminderMarkDeclaration` 明说带不带 mark 都可能是数据)——把安全判定藏进
296
+ 一个显示层剥壳函数里,才是下一个 bug。
297
+ - **🔴 [4974] 候选③:分类器 deny 的三个铸点、三种前缀**(core 5.52.0 dist 逐字枚举):
298
+ ① `hooks.js:606` 基础形;② `prepare-task.js:3103/3192` 继承自祖先层形;③ `prepare-task.js:3023`
299
+ 「批准过的编辑」在祖先层被拦形。三者尾巴同形(`: reason` 或 `: [category]`)。
300
+ 修前:② 被 ① 的前缀腿蒙对但 **reason 整条丢失**(tail 以 ` at an inherited…` 开头),③ **完全
301
+ 不识别**。修:三形收成一张**逐字**表,**长的排前**(短的是长的前缀,顺序即语义),命中后按同一
302
+ 条尾巴规则取 reason。逐字锚不放宽成模糊匹配(B8 ALLOW 登记的既有纪律,core 改文案必须跟车)。
303
+ 🔴 **前缀不是语法**(七审 finding③ 采纳):命中之后还要看**边界** —— 铸点的尾巴只有两种(到此
304
+ 为止,或紧跟 `': '`)。少了这条,`…ancestor layerX` 之类近似串也会被判成分类器裁决,而那条路径会
305
+ `markEnded` + 摘 HOLD + 写 Recent Denials + 用裁决原文覆盖渲染 —— 任何能产出工具错误正文的工具
306
+ 都能伪造。
307
+ 门:pure ㊲ 段(带 mark 剥壳 2 + 裸标签负控 1 + 伪造属性/坏 mark 形两条负控 2 + 三铸点矩阵 3 +
308
+ 形近串负控 1 + 边界闸四条 4)/ B6 delegated 段(带 mark 信封剥壳 1 + 只有信封时留白 1 + 未闭合
309
+ 保守不动 1 + 伪造属性负控 1),下限 B2 302→315 / B6 370→374。
310
+
311
+ - **🔴 [4970] 件⑤:连坐 abort 帧的判据认不出**旁观者**(0.40.0 机读位在现网零命中)**:
312
+ hold-poison 判据锚的是 `tool_end.output === "Operation aborted"` 精确串,而 core 的**两个短路点
313
+ 产两种文案** —— gate 主角(abort 早退)= 精确串;**同批被连坐的旁观者**(执行前短路)=
314
+ `"operation aborted before execution"`(小写 + 后缀)。⇒ 判据对旁观者**恰好全不命中**:它们的
315
+ 毒化帧根本没进 HOLD(当场上屏 + 被通用收口臂记 `markEnded`,resume 后重放的真结果被静默去重),
316
+ 于是 0.40.0 那个 `_sema_collateral_abort` 机读位在**真实现网一次都没出现过**
317
+ (test 线真机取证:真 server 7.41.0 + 真 Postgres + client-core 0.40.0,1 Bash 待审批 +
318
+ 2 并行 WebFetch,G1 零 stamp)。本文件旧注写的「core 对被 gate/**连坐** abort 的 call **恒**铸
319
+ 这一串」对旁观者不成立,已按实证撤回。
320
+ 修:判据改**双判据 OR**(单源 `isEngineAbortToolEnd`,`isGatedToolEnd` 与 hold-poison 臂两处
321
+ 同批换)——①**主锚** `tool_end.errorCode === "gate.parked"`;②**兜底**=既有精确串。
322
+ 🔴 **主锚是契约不是巧合**(core 官方答复 [4973]):两个 abort 短路点**消费同一个分类席**,而
323
+ park 提交**先于** abort 发射 ⇒ 铸点读那一席无竞态,主角与旁观者**恒**同席同码。(SDK 的
324
+ `Event_tool_end.errorCode` 声明处也逐字点名这个码;供给点见 core 5.52.0 dist `prepare-task` 的
325
+ `abortResultDetails: () => ({code:"gate.parked"})`。)
326
+ 🔴 **兜底臂有现役覆盖面,不是遗留兼容**([4973]②):core 现役**唯一不铸**本码的臂 =
327
+ **post-hook signal re-check**(工具**已经执行完**、hook 之后签核那一拍才发现 abort,历来铸的
328
+ abort 结果不带 details ⇒ 无 errorCode)。少了兜底臂,这条臂整条落空 —— 双判据设计正为它而立。
329
+ 🔴 两臂都**不放宽成模糊匹配**:主锚只认那一个**具名码**(「有 errorCode 就算」会把
330
+ `path_not_in_root` 这类普通工具错拖进 HOLD —— 正是 [2084]①-b 收窄掉的病),兜底不认文案家族。
331
+ 两臂都不命中 = 没有「因 gate 中止」的任何证据 ⇒ 照旧普通收口(老 core + 变体串就落这一格,
332
+ 诚实缺席)。
333
+ 🔴 **上游不会去合并两条串**([4973],core 显式拒绝改串向 —— **不是**「core 待修」):两串各承
334
+ 真语义(`"Operation aborted"` = 执行中被中止 / `"operation aborted before execution"` = 从未执行)。
335
+ 🔴 **本判据只锚 errorCode、不锚任何 call id**([4973]③):plan-review / resource-slice 这类不持有
336
+ call 的 park 照样带本码但诚实缺席 id —— 顺手锚 id 就把那几族的连坐帧判成「认不出」。
337
+ 🔴 **臂间掩蔽面同批堵住**(四审 finding① 采纳):放宽的是 HOLD 那条**超集**判据,而 ②`deny-stamp-next`
338
+ 用的是一次性 + **位置性**的全局记号(「下一张 gated 报错帧就是那张被拒的门」)—— 超集谓词会让
339
+ 同批连坐的旁观者**先到先吃**:它被改写成「用户拒绝了」并记 ended,真正被拒的 call 反倒只拿到引擎
340
+ 原文([2393] hitl-F1 的用户面,方向还反了)。⇒ 那条臂改用**窄口**(名字腿 ∪ 确切串 = 件⑤ 之前
341
+ 那一版逐字),新加的连坐码臂刻意不进来。
342
+ 门:**F11-a**(G1 主形)/ **F11-b**(兜底臂 = post-hook re-check 形)/ **F11-c、F11-d**(双负控:
343
+ 无码变体串、表外码普通错 —— 判据锚在帧的**位置**(零滞后上屏)而不只是文案,变异 M10 实撞)/
344
+ **F11-e**(旁观者先到也抢不走 deny 记号)。
345
+
346
+ - **🔴 [F4957-1] `approvalsFeed` 逃逸计数跨 push 重连残留**:`consecutivePollEscapes` 的唯一清零点
347
+ 是「轮询跑完一整拍」,而 push 腿**重连**那条臂(`runStream` 的 `mode !== 'push'`)停掉轮询腿却
348
+ 不动这个计数 ⇒ 被一整段**健康 push 窗**隔开的两批逃逸被当成「连续」累加。后果不是多算几次:
349
+ 刚从 push 回落的轮询腿才逃逸两拍就撞上限(`MAX_CONSECUTIVE_POLL_ESCAPES=5`),`mode()` 谎报
350
+ idle —— 而文件头卖给端的自检正是「跑了 N 秒之后 mode 不是 idle」,它会指着一条**刚活过来**的腿
351
+ 说它停摆了。修:重连一拍与「轮询跑完一整拍」是同等强度的健康证据(端真的从服务端收到了事件),
352
+ 同批清零 —— 计数的语义是**连续**,就只能被真正连续的失败喂大。
353
+ 🔴 **在途拍同治**(异源复审 finding③ 采纳):清零只管已经落地的计数,而一条**早于重连起跑**的
354
+ poll 可能还挂在 `await take()` 里,它稍后逃逸时会把计数从 0 又推回 1 —— 那一个单位是**上一条腿**
355
+ 的失败,却活到了下一次回落之后(回落后只需 4 拍就转 idle)。修:轮询腿带**代际**(重连即翻篇),
356
+ 非当代的完成回调既不改计数也不续排(续排归当代那条链)。
357
+ 🔴 **代际闸排在提交之前**(二审 finding② 采纳):守卫若只挡「计数与续排」,`take()` 早就已经改完
358
+ `lastDigest`/`revision`/`last` 并通知了订阅者 —— 于是「旧 poll 在途 → push 重连并发布新快照 →
359
+ 旧 poll 才带着**更老**的结果返回」这条乱序路径上,端会拿到一张 revision 更高、内容更旧的
360
+ `mode:'poll'` 快照(刚出现的待审批被抹掉,或刚解决的又冒回来)。闸前移到提交面。
361
+ 🔴 **乱序闸盖住每一条取件路径**(三审 finding② 采纳):`refresh()`(端上「用户点了刷新」)走同一个
362
+ `take()` 却不带代际 ⇒ 同一条回滚路径原样重现。闸从「代际」升成**单调请求序号**(发号 + 提交前核
363
+ 「有没有更晚发起的请求已经落地」),poll / push / refresh 三条路共用一把 —— 乱序是取件面的通病,
364
+ 不是轮询腿的私事。
365
+ 门:`run-hitl-gate-honesty-test.mjs` **F5-d / F5-e / F5-f / F5-g**(判据按 `list` **调用序号**编脚本、在途拍由
366
+ 测试显式放行,逃逸次数是确定值不随机器负载漂;负控 = 既有 F5「真·5 连逃逸 ⇒ 诚实转 idle」与
367
+ F5-c「单次逃逸归零」原样绿)。
368
+ - **🔴 [F4957-2] `engineAgentPanelStore` 缓冲区无界**:满员臂只找 `tick` 驱逐,**找不到也照样
369
+ `push`** ⇒ 一个 tick 都没有的场景里 `MAX_BUFFER=200` 形同虚设。这不是理论形:`fleet-row` 是
370
+ 后台子代在面板上的**唯一**在场 lane(它们的进度根本不走 `task_progress`),面板长期未挂载 +
371
+ 后台子代活跃 = 宿主内存被 UNTRUSTED wire 事件按条吃(实测 500 条 fleet-row 全部留在表里)。
372
+ 修:按**事件类别**而不是单一 kind 定规则 —— PROGRESS(`tick`/`fleet-row`,累计 rollup)按
373
+ (kind, taskId) 合并、满员时让位;LIFECYCLE(`end`/`sweep`)照旧必活(w0zwpa251 #4:被挤掉的
374
+ `end` 让面板行永远停在 running)。整表皆 LIFECYCLE 的极端形按 FIFO 顶掉最老一条 —— 有限缓冲下
375
+ 只能保住「最新」,而每 turn 末必发的 `sweep`(语义 = 结掉所有还在跑的行)恰恰就是最新那条。
376
+ 顺带:`fleet-row` 从此与 `tick` 同款按 taskId 合并(此前同一个后台子代每一拍都占一格)。
377
+ 🔴 **lifecycle 是硬屏障,屏障两侧的 progress 一条都不合并**(二审 finding② + 六审 finding② 采纳):
378
+ 就地覆盖会把 `progress(A,v1) → end(A) → progress(A,v2)` 重写成 `progress(A,v2) → end(A)`(复活进度
379
+ 被搬到结算之前);而只把顺序修对(摘旧条、新条入队尾)仍然**吞掉屏障之前那一拍** ——
380
+ `tick(v1) → sweep → tick(v2)` 压成 `sweep → tick(v2)` 时,直发是「建行 → 结算 → 迟到 tick 被消费端
381
+ 忽略」= 行留终态,回放却成了「对空表 sweep → v2 建出一条 **running** 行」= 幽灵在飞行。
382
+ 判据是消费后的**终态等价**,不是事件数组长得像 ⇒ `sweep`(全局)与同 task 的 `end` 都是屏障。
383
+ 屏障多占格子是对的取舍:上界仍由驱逐策略守着,而错折叠是**永久**静默错。
384
+ 🔴 **合并必须逐键继承**(四审 finding③ 采纳):两条 progress lane 发的都是**部分帧**(投影层
385
+ `...(toolUses !== undefined ? { toolUses } : {})`),缺席的语义是「这一帧没说 ⇒ 保留行上已有值」,
386
+ 粘性由消费端持。所以「后一条整只顶掉前一条」会把只在前一条出现过的键**永久丢掉**(面板一直挂着
387
+ 时看得到 `toolUses`,回放却看不到)—— 本段自己立的「回放 ≡ 直发」不变量当场被破。合并改成
388
+ **逐键继承**(后到者赢,后到者缺席的键继承先到者),位置仍取最新那条。
389
+ 🔴 **跨 lane 不合并**(五审 finding② 采纳):两条 progress lane 的消费规则不同 —— fleet-row 臂头注
390
+ 写死「tick 一到就永久让位」。`fleet-row(v1) → tick → fleet-row(v2)` 若压成 `tick → merged`,直发时
391
+ **让位之前**落地的 v1(transcriptId/startedAt 这类只此一帧的位)在回放里永远到不了消费端。
392
+ ⇒ 同 taskId 的**另一条 lane** 事件是合并屏障(倒序找合并目标,撞到对面 lane 即停)。
393
+ 🔴 **相邻 sweep 去重**(二审 finding③ 采纳):sweep 自述幂等(结掉所有还在跑的行),两条之间什么
394
+ 都没发生时第二条是严格 no-op。不去重的代价不是多一格 —— 面板长期未挂载时每 turn 一条 sweep 能把
395
+ 200 格**全占满**,此后屏障之后的复活进度一律让位,那一行在回放里永远停在 completed。
396
+ 门:`run-client-core-pure-test.mjs` 段 **⑧b**(有界 / 有界≠清空 / 合并 / 满员时 lifecycle 仍进得来
397
+ / tick 合并不回归 / end 屏障与 sweep 屏障各一条时序钉 / 两条 lane 各一条逐键继承钉 / 跨 lane 屏障钉
398
+ / 相邻 sweep 去重 + 全 lifecycle 满员后的复活进度仍进得来),下限 B1 225→238。
399
+ - **🔴 [F4947-1] `HitlBridge.findPending` —— 单源闸的撞名绕行口**(test 线 P0-KPI 复现件):
400
+ 「哪一行 pending 是人正要决定的」这段查找,REF-CC-029 已把三份近似复刻收编成单一导出
401
+ `findPendingForTask`,P-30 又只给**那一个**加了门种闸(把 `plan_review`/`resource_limit` 挡在
402
+ 「任意行」兜底之外)。但类里还留着一个**同名私有** `findPending`,而真实生产入口 ——
403
+ CC 兼容适配器 `makeHitlCanUseTool` → `decideTool(outcome, toolUseID)`,从不传
404
+ `preResolvedPending` —— 走的正是这条没有闸的老路:同 task 停着 plan_review 行时,它被当成
405
+ 「这个 run 的 pending」交给工具审批 decide(空 toolName 卡 / 409 `gate_not_tool_approval`)。
406
+ 修:私有件**删除并改调单源**(不补第二份过滤 —— 复刻正是这条缺陷的成因),名字一并换成
407
+ `pendingForDecide`(撞名正是「看起来只有一份实现」的成因);它那条**更强的身份腿**
408
+ (`toolUseID` 逐字命中)作为 `findPendingForTask` 的新 `callId` 参数带过去,同样只在过完门种闸
409
+ 的行里找。🔴 失败形分层保住:`no_pending`(良性缺席)才回 null,`approvals.list` 真失败照旧抛
410
+ —— 那个码在 `parkResolver` 里的语义是「这张 gate 已被解决 ⇒ 继续读流」,把网络失败塞进去 =
411
+ 谎报已解决(#110 烧会话那条路径)。
412
+ 🔴 **身份腿要过族闸,且族闸读整行**(四审 finding② + 五审 finding① 采纳):门种闸只挡
413
+ plan_review/resource_limit 这类**外族**,AskUserQuestion 与工具审批**同在**可决集合里 —— 身份腿
414
+ 若只比 taskId+callId,一个陈旧/错配/被复用的 park 身份就能让 fs 腿选中一行 ask(反之亦然),
415
+ 那一行随后原样当 `preResolvedPending` 去呈卡 + decide。身份与本腿族属冲突 ⇒ **忽略该身份**、
416
+ 照旧走族谓词回落(不动「身份缺席不 fail-closed」的政策:冲突是数据自相矛盾,不是查无此行)。
417
+ 🔴 族闸必须**读整行**而不是只读 toolName:工具审批族有**两条腿**(一等 `kind==='tool_approval'`
418
+ 放行任意 toolName + fs/shell 名字腿),而 wire 传下来的 `matches` 只有名字腿那半 —— 拿它当身份闸,
419
+ **合法的 kind-only 行**即使逐字命中也会被判出局,回落挑了同 task 的另一行(卡面与决断全落在别的
420
+ 审批上、真 gate 继续 suspended)。⇒ 身份闸由调用方按自己那条腿的**完整**族判据供给
421
+ (fs 腿 = `isToolApprovalGate({kind: row.gateKind, toolName})`,ask 腿 = `isAskTool`)。
422
+ 🔴 **身份腿不许跨 run**(二审 finding① 采纳):`/v1/approvals` 是 scope 级单队列,身份腿同样约束在
423
+ 本 `taskId` 之内 —— 一个撞名/被复用的 callId 否则就能把**别人会话**的 pending 行交给这次 decide
424
+ (REF-CC-022 拆掉 `?? pending[0]` 防的正是这件事)。**刻意不 fail-closed**(该项部分驳回,理由
425
+ 成文于参数头注):身份在场而队列没有那一行时照旧回落 —— 回落的失效面与本参数出现之前逐字节相同,
426
+ fail-closed 的失效面则是「每一次审批都决断不了」,而这个 join 本仓没有对真 server 的实证。
427
+ 🔴 **族冲突 ⇒ 这一轮候选池整体收紧到族闸放行的行**(六审 finding① + 七审 finding① 采纳):
428
+ 只拦身份腿是自相矛盾的(「同 taskId 任意行」兜底会把**同一行**重新捞出来);只剔除命中的那**一个
429
+ 对象**也不够 —— 同 task 排着两条反族行时,兜底照样捡起第二条。冲突 = wire 数据自相矛盾
430
+ (park 说 gate 属 A 族、身份却指着 B 族的行),这一轮整体保守:只在族闸放行的行里选,一条不剩 ⇒
431
+ 诚实 `no_pending`(走既有 fail-soft / 重呈)。🔴 收紧**只在冲突路径上**发生 —— 身份缺席时候选池
432
+ 一字节不动(那条「任意行」兜底本来就是给 kind-only / 名字腿认不得的合法行留的)。
433
+ 门:**F9-a/b/c/d/e** + **F10-h**(身份命中反族行 ⇒ 忽略身份)+ **F10-i**(kind-only 行的身份命中
434
+ 照样算数 —— 族闸认一等 kind 腿)+ **F10-j/k**(队列只剩反族行 / 排着两条反族行 ⇒ 零 decide)。
435
+ - **🔴 [4933] 存量:决断成功臂 `dropHeld` 摘错扣留帧**(0.40.0「已知局限」里那条记案,本版销案):
436
+ 决断成功那一下用的是 `findPendingForTask` 给的 callId —— 那是**卡面渲染的宽口**(同 taskId +
437
+ 族谓词命中的第一行),不是身份;同族多行排队时它可能指向旁观者,于是被摘掉的是**另一条** call
438
+ 的扣留帧,而那一帧从此不再吐(`flushHeld` 的「一帧不丢」在这一格上不成立 = dim 圆点永远等不到
439
+ 结果行)。修(新动词 `dropHeldForDecidedPark`,两条腿):①**wire 身份**在场就点名摘 ——
440
+ server ≥7.41.0 的 `done{status:"suspended"}.result.toolCallId` 与 durable `suspended` 事件
441
+ `gate` 旁的同键([4913]/[4914] 请托① 到货件),两条 park 腿各读各的载体、判据同一只,
442
+ UNTRUSTED 值只认非空串(坏形按缺席回落);②缺席 ⇒ 只在候选于扣留表里**同族唯一**时才摘
443
+ (族的口径与取件谓词同轴:fs/shell 族看族成员集,其余看 toolName),同族多条 = 判不出主角 ⇒
444
+ **一条都不摘**,与 `flushHeld`「判不出整表不标」同轴的 fail-safe。
445
+ 🔴 **身份必须从取件那一步就起作用**(异源复审 finding① 采纳):只拿它删帧、决断仍按队列首行走
446
+ = split-brain(决断绑定落在旁观者行、被删的是主角帧)—— 用户其实在批准/拒绝**另一个**工具调用,
447
+ 而真 park 原地不动、反复重挂。所以 `park.gatedCallId` 一路传进两条取件腿
448
+ (`surfaceFsApprovalAndDecide` / `surfaceGateAndDecide` → `findPendingForTask` 的 `callId` 腿),
449
+ 决断与删帧从此指向同一行;删帧那一步的 wire 身份因此降为**确证**(与真被决断的那一行相等才摘),
450
+ 不再是第二个独立的摘取目标。**刻意不 fail-closed**:身份在场但队列里没有对应行 ⇒ 照旧回落既有
451
+ 两条腿 —— 这个 join 本仓没有对真 server 的实证,而 fail-closed 的失效面是「每一次审批都决断不了」,
452
+ 回落的失效面则与本键出现之前逐字节相同(代价不对称,方向倒向回落)。
453
+ 🔴 **刻意不动**两处:`markDecided` 仍用 pending 行的 callId(它喂的是 #110 那条承重判据,候选
454
+ 取自 `lastFsOrShellGatedCallId()`,两边必须同源);#324 的连坐判别子**不吃**这个新身份 ——
455
+ 换锚是独立一件事,混在一起改就分不清谁引入了假阳。门:**F10-a..f**(含「决断绑定落在主角行、
456
+ 旁观者一个字节没被碰」两条;既有 F8-S33 的期望串同批跟改:旧串只有一条正是因为旁观者的帧被
457
+ 摘掉了,判词本身一字未变)。
458
+
459
+ **候上游到货的跟车项(本批不做)**:
460
+
461
+ - **core 5.55 `tool_end.gatedCallId`(顶层键;勘误:不在 details 面)**:到货后可给连坐簇的
462
+ **折叠归组**面升级(按 gate id 归组,而不是只按「本批」)。与本批的**识别**判据(errorCode)是
463
+ 两件事 —— 识别不许锚 id(见 [4973]③),归组才用 id。届时另批跟车。
464
+
465
+ **已知局限(本版新增)**:
466
+
467
+ - **面板缓冲到顶时,最老的那条 lifecycle 会被挤掉**(七审 finding④ 处置后的**残余**):到顶必须有
468
+ 东西让位,规则统一成**最新优先** —— 没有 progress 可让位时让最老那条(不论类别)。此前那版让
469
+ **进度**让位,而屏障之后的一条 progress 往往是某一行「新建/复活」的**唯一**一句话
470
+ (`projectFleetAgentRows` 只在变化时发帧),丢了它那一行在重挂后永远不存在 —— 那是更坏的丢法。
471
+ 残余代价:一条 ~200 事件之前的 `end` 可能被挤掉(连同它的 `report`/`isError`);它要结的那一行由
472
+ 每 turn 必发、且必然最新的 `sweep` 兜住。真正的根治是把缓冲从「原始事件日志」换成**按任务压实的
473
+ 状态**,那会整体改写回放语义,自成一批,不搭 bug 修复批的车。
474
+ - **#324 判别子的 S3/S33 局限仍在**:同族多兄弟时「哪个撞了 gate」在**判别子**那一侧仍按诚实缺席
475
+ 处置(整表不 stamp)。本版把 wire 身份接进来的只有 `dropHeld` 这一格 —— 判别子换锚要连着
476
+ `noteParkGate` 的证据结构一起动,单独排批。常驻门 `F8-S3/S33` 仍钉「全 ABSENT」。
477
+ - **`fleet-row` 缓冲合并按 (kind, taskId)**:同一个 taskId 的 `tick` 与 `fleet-row` **不**互相合并
478
+ (两条 lane 的可选性不同,消费端对它们的规矩也不同 —— 见 `EngineAgentPanelEvent` 的 fleet-row
479
+ 臂头注)。面板未挂载期间同一行最多各留一条,这是刻意的。
480
+
31
481
  ## 0.40.0(2026-08-22)
32
482
 
33
483
  **minor:连坐 abort 机读位 `_sema_collateral_abort`(#324 / [4907] 裁定)。**
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.40.0
38
+ **Version:** 0.42.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -245,6 +245,26 @@ export function eventToSdkMessage(ev, ctx) {
245
245
  // (core 不鉴别、不比对、不回读)—— 只渲染,绝不回喂模型/工具入参,更不许当鉴权判据。
246
246
  const toolEndSettledBy = ev.settledBy;
247
247
  const toolEndApprover = ev.approver;
248
+ // ── A-D2(0.42.0,cli wire 锚台账 cli-191 镜头 A):`resolution`(core ≥5.35 `AskDenyResolution`)
249
+ // —— 这次 deny **是哪一条拒绝臂**(`human_refused` / `window_expired` / `no_approver` /
250
+ // `blanket_allow_refused` / `approver_unavailable` / `task_aborted` / `presentation_failed` /
251
+ // `approver_error` / `approver_contract`)。与 {@link toolEndSettledBy} **合读而不互替**:
252
+ // settledBy 只说「这次等待是哪一**种**收场」,resolution 分类的是**拒绝臂本身**。
253
+ // 🔴 为什么这一位非补不可(消费面已经在等它):要判「审批窗口自己走完了」,单锚 settledBy 会把
254
+ // 「审批方违约」那一臂一起收进来(approver 回了契约外的东西,窗口并没有自然走完)。本臂此前
255
+ // 是闭形白名单,`resolution` 在包边界被整段剥掉 ⇒ 消费端**缺一臂**,只能退到锚 core 散文
256
+ // (文案一改就漂)。[1947] `toolCallId` / [2942] `governanceForced` / #263 两键 / #324 机读位
257
+ // 之后的**同款病形第五例**。
258
+ // 🔴 **给消费端的判据是正面匹配**(异源复审 [medium] 采纳):
259
+ // `settledBy === 'timeout' && resolution === 'window_expired'`;**绝不**写
260
+ // `resolution !== 'approver_contract'` —— 那个不等式对**缺席**(旧引擎)与**任何未来新码**
261
+ // 都为真,会把「不知道是怎么拒的」折成「窗口自然结束」。
262
+ // 🔴 **开集防御读**(与上面三键同姿势):类型面虽是九词闭集,词表属主仍是引擎 —— 窄读成枚举
263
+ // 会在加员当天把一个合法值判没。非空串即原样透传,绝不按识别表过滤、绝不 trim/截断/改写。
264
+ // 🔴 **缺席不带语义,不许反推**(core 头注逐字):每一次真正执行了的调用、每一次 policy/hook
265
+ // 直拒、以及 durable/decide 腿的结算(它们带自己的 settledBy/reason)都**不带**这一位。
266
+ // 缺席 ≠「不是拒绝」也 ≠「人拒的」⇒ 缺席就不铸键(绝不折成 null / 空串)。
267
+ const toolEndResolution = ev.resolution;
248
268
  // ── #324 半场(0.40.0):连坐 abort 机读位 `_sema_collateral_abort`([4907] 裁定)──
249
269
  // 出处**不是引擎**,是本包自己的 HITL 桥:`gateLedger.flushHeld` 对「出身 = collateral」的
250
270
  // 扣留帧 stamp(见 `SEMA_COLLATERAL_ABORT_KEY` 头注)。桥在本臂**上游**(宿主先用
@@ -281,6 +301,10 @@ export function eventToSdkMessage(ev, ctx) {
281
301
  ...(typeof toolEndApprover === 'string' && toolEndApprover.length > 0
282
302
  ? { approver: toolEndApprover }
283
303
  : {}),
304
+ // A-D2:拒绝臂分类(开集、缺席不铸、在场原样;判据本体是上方那段头注)。
305
+ ...(typeof toolEndResolution === 'string' && toolEndResolution.length > 0
306
+ ? { resolution: toolEndResolution }
307
+ : {}),
284
308
  // #324:连坐 abort 机读位(严格 true 才上臂;见上方头注)。
285
309
  ...(collateralAbort === true ? { _sema_collateral_abort: true } : {}),
286
310
  ...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
@@ -29,7 +29,7 @@
29
29
  * false」—— 对这一族,发 `false` 是违约(核心根本没有「显式关」这个语义位),所以本模块对
30
30
  * 它们**永不产出 false**:OFF ⇒ 键不出现。
31
31
  *
32
- * ② **explicit-false 族**(2 员:agentListing / skillsListing):
32
+ * ② **explicit-false 族**(3 员:agentListing / skillsListing / **backgroundTasks**):
33
33
  * 它们在 core 里是 **DEFAULT-ON**,SDK 头注写死「`agentListing`/`skillsListing` 是 core
34
34
  * DEFAULT-ON —— **explicit false 才关**(1.254 起 false 真透传;更老 server 静默丢)」。
35
35
  * 于是「删键」在这一族上的含义是**保持开着**,而不是关掉。旧的 `AttachmentsSpec` 把全部键
@@ -37,6 +37,29 @@
37
37
  * 路径可以关闭**(不是「难关」,是类型层封死)。本批把这两键的类型放宽成 `boolean` 并给出
38
38
  * 产出 `false` 的路径。
39
39
  *
40
+ * ══ 🔴 [4982] 候选①(P0-KPI,0.42.0)—— **第三员漏族:`backgroundTasks`** ════════════════════
41
+ *
42
+ * core **5.12.0 (BREAKING)** 把 `attachments.backgroundTasks` 与 `agentListing`/`skillsListing`
43
+ * **同批**翻成 DEFAULT-ON(core `types.d.ts` 逐字:「Post-compact background-task restatement —
44
+ * DEFAULT ON since 5.12.0 (**boolean, not `true`**: explicit `false` is the opt-out; **same contract
45
+ * as the listing family below**)」)。三兄弟同批翻转,本模块**跟修了后两个、漏了第一个** ——
46
+ * `backgroundTasks` 一直留在 ① 族(off = 删键,永不发 false)。
47
+ * 后果是这个旋钮上最坏的一种失效:`SEMA_ATTACHMENTS=off`(文件自称「真·全关」)对这一位与
48
+ * **什么都不配**逐字节同效(两条路都不发这个键 = 都是「开」),**且零报错** —— 用户以为关掉了,
49
+ * 附件照进上下文,没有任何一处会说出来。与 ② 族当初那个洞是同一个病形的第三例。
50
+ *
51
+ * ── 修法:把「能不能显式打开」与「能不能显式关掉」拆成**两条正交的轴** ────────────────────────
52
+ * `backgroundTasks` 同时住两族,这不是分类含糊,这**就是**它的契约:
53
+ * · **轴 A(可显式开)** = `full` 会全开的那 7 员 —— `backgroundTasks` 在内,**membership 不动**;
54
+ * · **轴 B(可显式关)** = core DEFAULT-ON 的 3 员 —— `backgroundTasks` 新入。
55
+ * 只把它整个搬进 ② 族(= 从轴 A 里删掉)是**错的修法**,两条理由都是硬的:
56
+ * ① `full` / `default` / `+backgroundTasks` 三条既有路会当场少发一个 `backgroundTasks: true`
57
+ * —— 对 core ≥5.12.0 行为等价(默认就是开),但对**更老引擎**那是真的从「开」变成「关」,
58
+ * 而本模块手里没有 server 版本这个量(见下面 §能力边界),没资格替用户做那个降级;
59
+ * ② 空 env 的默认产出 `{backgroundTasks: true, toolsDelta: true}` 是 [487]② CC-parity 那一对的
60
+ * 字面承诺,删掉它就是把「默认逐字节不变」这条本模块的立身纪律自己破了。
61
+ * ⇒ 本批**只加一条出路**(`off` 与 `-backgroundTasks` 产出 `false`),其余取值逐字节不变。
62
+ *
40
63
  * ── env 面(`SEMA_ATTACHMENTS`)的取值语法,以及为什么选这一种 ────────────────────────────────
41
64
  * 目标是「**默认行为逐字节不变** + 有路可关 DEFAULT-ON 两键」,所以选了**在既有值上加逗号 token
42
65
  * 列表**,而不是新开一个 env:
@@ -45,8 +68,9 @@
45
68
  * · 逗号 token 列表对**存量取值零影响**:`unset` / `full` / `all` / off 拼法四种老写法解析结果
46
69
  * 逐字节不变(FIX7 段 ⑤ 有正面钉),新语法只在用户真写了逗号 token 时才被触发。
47
70
  * 语法(逗号分隔,大小写不敏感,允许空白):
48
- * · `off|0|false|no|none`(整值) ⇒ **真·全关**:literal-true 族一个不发(那一族的 off 就是
49
- * 删键)+ DEFAULT-ON 两键发 `false`。
71
+ * · `off|0|false|no|none`(整值) ⇒ **真·全关**:只可显式开的那一族一个不发(它们的 off 就是
72
+ * 删键)+ DEFAULT-ON **三键**(agentListing / skillsListing /
73
+ * backgroundTasks)发 `false`。
50
74
  * 🔴 对抗复审 [medium](2026-08-07)判的真病:这一支此前直接 `return undefined`(不发字段),
51
75
  * 而「不发字段」对 DEFAULT-ON 两键的含义**恰恰是保持开着** —— 一个叫 off 的总开关关不掉十件里
52
76
  * 的两件,且用户没有任何办法察觉。旧行为在 <1.254 的 server 上与新行为**逐字节等价**(那些
@@ -60,8 +84,9 @@
60
84
  * · `full` / `all` ⇒ literal-true 族 7 员全开(**不含** DEFAULT-ON 两键 ——
61
85
  * 不发 = 保持 core 默认开,壳不替 core 做那个决定)
62
86
  * · `default` ⇒ CC-parity 那一对(显式写出「默认」这个意思)
63
- * · `<key>` / `+<key>` ⇒ 单点打开一个 literal-true 族成员
64
- * · `-agentListing` / `-skillsListing` ⇒ 产出 **`false`**(explicit-false 族唯一的关法)
87
+ * · `<key>` / `+<key>` ⇒ 单点打开一个「可显式开」族成员(轴 A 的 7 员)
88
+ * · `-agentListing` / `-skillsListing` / `-backgroundTasks` ⇒ 产出 **`false`**(DEFAULT-ON 三键
89
+ * 唯一的关法;`-backgroundTasks` 是 [4982] 候选① 补的那条)
65
90
  * · `todoReminderMode=baseline|off` ⇒ 取值型键
66
91
  * 🔴 未登记 token / 对 literal-true 族用 `-` / todoReminderMode 取闭集外的值 ⇒ **fail-loud 抛错**。
67
92
  * 静默忽略是这条链上最坏的失效形:用户以为自己关掉了 agentListing,而请求照发、附件照进上下文,
@@ -88,7 +113,12 @@ export interface AttachmentsSpec {
88
113
  };
89
114
  planModeReminder?: true;
90
115
  budgetUsd?: true;
91
- backgroundTasks?: true;
116
+ /**
117
+ * 🔴 **两族兼属**([4982] 候选①,0.42.0):它在轴 A(`full` 会开)也在轴 B(core ≥5.12.0
118
+ * DEFAULT-ON,`false` 才是关)。类型因此是 `boolean` 而不是 `true` —— 钉成 `true` 正是
119
+ * 「关不掉」那个洞的类型层根因。
120
+ */
121
+ backgroundTasks?: boolean;
92
122
  toolsDelta?: true;
93
123
  mcpInstructions?: true;
94
124
  agentListing?: boolean;
@@ -96,8 +126,13 @@ export interface AttachmentsSpec {
96
126
  }
97
127
  /** 键面台账(机读位):加键必须同批登记,否则下方 `Covers` 双向钉编译红。 */
98
128
  export declare const ATTACHMENTS_SPEC_KEYS: readonly ["todoReminder", "todoReminderMode", "changedFiles", "planModeReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "mcpInstructions", "agentListing", "skillsListing"];
99
- /** ② 族(core DEFAULT-ON,explicit false 才关)。 */
100
- export declare const ATTACHMENTS_DEFAULT_ON_KEYS: readonly ["agentListing", "skillsListing"];
129
+ /**
130
+ * **轴 B —— 可显式关**(core DEFAULT-ON,`false` 才是关;`-<key>` 与总开关 `off` 的作用面)。
131
+ * 🔴 [4982] 候选①(0.42.0):`backgroundTasks` 补入 —— core 5.12.0 与另两员**同批**翻 DEFAULT-ON,
132
+ * 本模块当时只跟修了两员。这张表**不是** ① 族的补集(`backgroundTasks` 两族兼属,见 AttachmentsSpec
133
+ * 上的键注与文件头注的「两条正交的轴」段)。
134
+ */
135
+ export declare const ATTACHMENTS_DEFAULT_ON_KEYS: readonly ["agentListing", "skillsListing", "backgroundTasks"];
101
136
  /**
102
137
  * ENV source → the `TaskRequest.attachments` stamp.
103
138
  *