@sema-agent/client-core 0.41.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.
package/CHANGELOG.md CHANGED
@@ -28,6 +28,244 @@
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
+
31
269
  ## 0.41.0(2026-08-23)
32
270
 
33
271
  **patch:七件真缺陷修(逃逸计数跨腿残留 / 面板缓冲无界 / 单源闸的撞名绕行口 / 决断成功摘错扣留帧 /
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.41.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
  *
@@ -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,10 +113,20 @@ export const ATTACHMENTS_SPEC_KEYS = [
88
113
  'agentListing',
89
114
  'skillsListing',
90
115
  ];
91
- /** ② 族(core DEFAULT-ON,explicit false 才关)。 */
92
- export const ATTACHMENTS_DEFAULT_ON_KEYS = ['agentListing', 'skillsListing'];
93
- /** 族里 `full` 会全开的那 7 员(`todoReminderMode` 是取值型,不搭 full 的车)。 */
94
- const LITERAL_TRUE_KEYS = [
116
+ /**
117
+ * **轴 B —— 可显式关**(core DEFAULT-ON,`false` 才是关;`-<key>` 与总开关 `off` 的作用面)。
118
+ * 🔴 [4982] 候选①(0.42.0):`backgroundTasks` 补入 —— core 5.12.0 与另两员**同批**翻 DEFAULT-ON,
119
+ * 本模块当时只跟修了两员。这张表**不是** 族的补集(`backgroundTasks` 两族兼属,见 AttachmentsSpec
120
+ * 上的键注与文件头注的「两条正交的轴」段)。
121
+ */
122
+ export const ATTACHMENTS_DEFAULT_ON_KEYS = ['agentListing', 'skillsListing', 'backgroundTasks'];
123
+ /**
124
+ * **轴 A —— 可显式开**:`full` 会全开的那 7 员(`todoReminderMode` 是取值型,不搭 full 的车)。
125
+ * 🔴 `backgroundTasks` **留在本表**([4982] 候选① 的取舍,理由见文件头注):把它从这里删掉会让
126
+ * `full`/`default`/`+backgroundTasks` 三条既有路少发一个 `true`,对 core <5.12.0 是真的从
127
+ * 「开」变成「关」,而本模块手里没有 server 版本这个量。
128
+ */
129
+ const OPT_IN_KEYS = [
95
130
  'todoReminder',
96
131
  'changedFiles',
97
132
  'planModeReminder',
@@ -100,9 +135,15 @@ const LITERAL_TRUE_KEYS = [
100
135
  'toolsDelta',
101
136
  'mcpInstructions',
102
137
  ];
138
+ /**
139
+ * **只可显式关、不可显式开**的那两员(轴 B ∖ 轴 A)。`+agentListing` 这类写法的 fail-loud 判据
140
+ * 锚在**本表**而不是整个轴 B —— 锚轴 B 会把 `+backgroundTasks` / `full` 一起误拒([4982] 候选①
141
+ * 修复批的判别力所在:两条轴各判各的,不许拿一条去代另一条)。
142
+ */
143
+ const DEFAULT_ON_ONLY_KEYS = ['agentListing', 'skillsListing'];
103
144
  /** CC-parity 默认对(TOC live 路径,[487]②)。 */
104
145
  const DEFAULT_PAIR = ['backgroundTasks', 'toolsDelta'];
105
- const _attachmentsKeyPins = [true, true, true, true];
146
+ const _attachmentsKeyPins = [true, true, true, true, true, true];
106
147
  void _attachmentsKeyPins;
107
148
  const TODO_REMINDER_MODES = ['baseline', 'off'];
108
149
  /** fail-loud 的统一措辞:说清**哪个 token 坏了**、以及合法形是什么(判词要指得出下一步)。 */
@@ -156,7 +197,7 @@ export function attachmentsForRequest(env = hostEnv()) {
156
197
  if (!isDefaultOnKey(key)) {
157
198
  refuse(token, `"${key}" belongs to the literal-true family whose OFF is "delete the key", not "send false" ` +
158
199
  '(core wire contract, board [479] ask-2) — simply do not turn it on. ' +
159
- 'Only the core DEFAULT-ON keys (agentListing / skillsListing) accept an explicit false');
200
+ `Only the core DEFAULT-ON keys (${ATTACHMENTS_DEFAULT_ON_KEYS.join(' / ')}) accept an explicit false`);
160
201
  }
161
202
  spec[key] = false;
162
203
  continue;
@@ -174,7 +215,10 @@ export function attachmentsForRequest(env = hostEnv()) {
174
215
  const key = canonicalKey(lhs.startsWith('+') ? lhs.slice(1) : lhs);
175
216
  if (key === undefined)
176
217
  refuse(token, 'unknown attachments key');
177
- if (isDefaultOnKey(key)) {
218
+ // 🔴 判据锚 `DEFAULT_ON_ONLY_KEYS`(轴 B ∖ 轴 A),**不是**整个轴 B:0.42.0 起
219
+ // `backgroundTasks` 两族兼属,拿轴 B 判会把 `+backgroundTasks` 一起误拒(而它在
220
+ // `full` 里本来就开得出来,拒它等于同一件事有两个答案)。
221
+ if (isDefaultOnOnlyKey(key)) {
178
222
  refuse(token, `"${key}" is core DEFAULT-ON — turning it "on" is a no-op that would send a redundant true; ` +
179
223
  `use "-${key}" to turn it OFF`);
180
224
  }
@@ -184,7 +228,8 @@ export function attachmentsForRequest(env = hostEnv()) {
184
228
  }
185
229
  return spec;
186
230
  }
187
- /** 总开关 off 的产出:literal-true 族一个不发(删键即关)+ DEFAULT-ON 两键显式 `false`。 */
231
+ /** 总开关 off 的产出:只可显式开的那族一个不发(删键即关)+ DEFAULT-ON **三键**显式 `false`
232
+ * ([4982] 候选①:`backgroundTasks` 从 0.42.0 起真的在这条路上被关掉)。 */
188
233
  function allDefaultOnOff() {
189
234
  const out = {};
190
235
  for (const k of ATTACHMENTS_DEFAULT_ON_KEYS)
@@ -193,7 +238,7 @@ function allDefaultOnOff() {
193
238
  }
194
239
  function allLiteralTrue() {
195
240
  const out = {};
196
- for (const k of LITERAL_TRUE_KEYS)
241
+ for (const k of OPT_IN_KEYS)
197
242
  out[k] = true;
198
243
  return out;
199
244
  }
@@ -206,9 +251,14 @@ function splitAssign(token) {
206
251
  function canonicalKey(lower) {
207
252
  return ATTACHMENTS_SPEC_KEYS.find(k => k.toLowerCase() === lower);
208
253
  }
254
+ /** 轴 B:可显式关(`-<key>` 的合法作用面)。 */
209
255
  function isDefaultOnKey(k) {
210
256
  return ATTACHMENTS_DEFAULT_ON_KEYS.includes(k);
211
257
  }
258
+ /** 轴 B ∖ 轴 A:只可关不可开(`+<key>` 的 fail-loud 作用面)。 */
259
+ function isDefaultOnOnlyKey(k) {
260
+ return DEFAULT_ON_ONLY_KEYS.includes(k);
261
+ }
212
262
  /** 单 token 形的历史回落判据(见 `attachmentsForRequest` 里那段注释)。 */
213
263
  function isKnownToken(token) {
214
264
  if (token === 'full' || token === 'all' || token === 'default')
@@ -52,8 +52,10 @@
52
52
  * 从 `GET /v1/approvals` 拿 pending 行(input=questions + boundCallId/boundInputHash 绑定)、
53
53
  * 合成 QuestionFrame 借既有 AskUserQuestion overlay(planReviewWire 同款 local-responder 姿势,
54
54
  * CC 原生对话框,零新 UI)、等用户作答。
55
- * 3. 决断走 T23 HitlBridge(答案 ride `ApprovalDecision.answer`,D-1 绑定 verbatim 回显;拒答=
56
- * cancel-by-deny,contract/04 §2.4)。decide SYNC 驱动的:引擎跑到下一个 park 或终态才返
55
+ * 3. 决断走 T23 HitlBridge(答案 ride `ApprovalDecision.answer`,D-1 绑定 verbatim 回显;拒答 =
56
+ * 一次 **TOOL 级 deny** 应答,把这只 ask 结算掉 —— server `ASSISTANT-WIRE-CONTRACT.md` §4a。
57
+ * 🔴 **0.42.0 撤稿**:原文写的是「拒答=cancel-by-deny,contract/04 §2.4」;§4a 逐字反对把
58
+ * deny 读成 run kill,逐条撤稿见 `hitlBridge.decideTool` 头注)。decide 是 SYNC 驱动的:引擎跑到下一个 park 或终态才返
57
59
  * (实测 4-5s+),返回体 `{status}` 即下一状态。
58
60
  * 4. 续流:attach `GET /v1/runs/:taskId/events`(durable leg;实证 durable log 从 park 点才开始,
59
61
  * 无挂起前重放)。重放的 tool_start/tool_end 按 toolCallId 去重;被 gate 的 call 在重放里带来