@sema-agent/client-core 0.57.0 → 0.59.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
@@ -49,6 +49,286 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.59.0(2026-09-06)
53
+
54
+ > 内容批(2026-09-06,隔离树交付):三件 —— ①`@sema-agent/sdk` **8.3.0 提货** + 审批帧键镜像与
55
+ > **卡键台账**双腿对账(peer 地板抬版,本版**唯一**的非 additive 面)、②`Capabilities` **74→93
56
+ > 逐键复核** + 能力位**四态通用读口** + **S-131 SQL 姿态读面从壳侧上收**(归层债)、③三件候件投影
57
+ > (`tool_end.autoDenied` / `denialLimitFallback` 上卡 / `wiring_manifest.mcp[]`)。**本段只记内容**:
58
+ > `package.json.version` 未动、README Version 行未动、`FROZEN` 未动 —— bump 与段头转日期形归
59
+ > **发包批**(阶段一义务见本档头注)。
60
+ > 接入面详报见 `docs/INTEGRATION-CLIENTS.md` **§23**(三件各一小节 + 端的迁移动作 + 黑盒判据 + 门表)。
61
+
62
+ ### 🔴 BREAKING(peer 地板)
63
+
64
+ - **`@sema-agent/sdk` peer 地板 `>=8.2.0` → `>=8.3.0`**(devDep `^8.3.0` 同批)。三条硬理由:
65
+ ① `RULE_OFFERS_ABSENCE_REASONS` / `DENIAL_LIMIT_KINDS` 两张闭词表是**值级** import,<8.3.0 上这两个
66
+ 名字不存在、整包编译不过;② type-only 的 `AskOrigin` / `RuleOffersAbsence` / `DenialLimitKind` /
67
+ `DenialLimitFallback` 四个型面(帧上 `denialLimitFallback` 与四个公面别名直接引用);
68
+ ③ 真正的量是三张锚 —— `TOOL_APPROVAL_FRAME_KEYS` 18→**27**、`APPROVAL_CARD_KEYS` 17→**19**、
69
+ `Capabilities` 74→**93**,分别由两道门逐元素对账。
70
+ 地板与 peer 声明由 `run-sdk-floor-test.mjs` **三位逐位等值**互绑,抬一处忘另一处当场红。
71
+
72
+ ### 件① sdk 8.3.0 提货 + 审批帧/卡键双腿对账
73
+
74
+ - **帧上三个新键**(全 additive,`"tool_approval"` only)进 `ToolApprovalFrame` 与键镜像(24→**27**),
75
+ 并**到得了卡口**(`ApprovalCardRequest` 同名三键;单源 = 活卡帧腿):
76
+ - `origin`(S-125③ / core 7.5.0 `ASK_ORIGINS` 闭八词,引擎在门上盖章)—— 🔴 **缺席不是
77
+ `"policy"`**(那是一个正面事实,折进去 = 替引擎编话);按**开集**分支,包内**零闭集校验**
78
+ (词表属主是 core,server 已按闭集拒过,包再校一遍只会在 core 加员当天把合法值判没)。
79
+ - `ruleOffersAbsence`(S-15 第五单 / core #490 修②,闭三词)—— 🔴 **缺席不是断言**;`mandated`
80
+ 臂**不得**指向「去写一条规则」;与 `ruleOffers` 引擎侧互斥,但**包不据此互删**(同时在场 = 上游
81
+ 坏形,原样上卡让端看得见,包替上游把矛盾湮灭掉只会让排障说不清是谁窄没的)。
82
+ - `denialLimitFallback`(S-114 / core 7.4.0 #548,四成员全必填)—— 走包内窄读器,**缺一整只丢**
83
+ (与 server `readDenialLimitFallback` 的 `.strict()` 同判据:半只对象上的计数会被人当成真实次数读);
84
+ 计数与窗按「有限非负数」判(`0` 是**合法读数**不折缺席),`limit` 按开集读。与
85
+ `requiresRealApproval` 是**孪生键**。🔴 `autoDenyAfterMs` **只渲倒计时,绝不据它自铸第二只定时器**。
86
+ - **两张闭词表运行期再导出**:`RULE_OFFERS_ABSENCE_REASONS` / `DENIAL_LIMIT_KINDS`(与 0.58.0 那三张
87
+ 同一条纪律 —— 端拿它做判定,别再手抄字面量;⚠️ 本包自己**不拿它们做窄读判定**,端的 `switch`
88
+ 仍必须带 `default` 臂)。
89
+ - **`AHEAD_OF_ANCHOR` 整表退役**:六条领先记账(`requiresRealApproval` / 窗三键 / `inputHasBidi` /
90
+ `parked`)**同一天到期**,删登记前先让它自红一次留证。✅ 表回空 = 健康态,镜像与锚逐元素相等。
91
+ - 🆕 **对账门加第二条腿(卡面腿)**:sdk `APPROVAL_CARD_KEYS`(19)逐键处置台账 —— 帧腿盖不住卡这
92
+ 一格(帧上有的事实要**到得了卡口**才有人看得见,[4851] 窗三键那次正是这条缝里漏的)。`carried`
93
+ 行必须真在 `ApprovalCardRequest` 上、`not_carried` 行必须真不在且写了理由,与锚**逐元素相等**。
94
+
95
+ ### 件② `Capabilities` 74→93 逐键复核 · 四态读口 · S-131 读面上收
96
+
97
+ - 🆕 **`scripts/run-engine-caps-ledger-test.mjs`**:93 键**逐键处置台账**门。病是「sdk 一版加了 19 个
98
+ 能力位,而本包一个字不动这件事在门盘上**一声不响**」。台账 ↔ SDK 键集**双向等值**(加位缺行红 /
99
+ 删位空转登记也红);`read` 行(14)的**代码位置**实证、`not_read` 行零读点、五形读点反向普查
100
+ (每形先在伪造样本上自证会说话 —— 初版只认一种调用形时 14 个读点只认出 7 个)。
101
+ - **新公面 `engineCapValue(baseUrl, key): EngineCapValueState`**(additive)—— 能力位的**四态**读口
102
+ `unobserved` / `not_reported` / `null` / `value`。既有三口把四种「读不出」全折成 fail-closed 一档,
103
+ 对**放行**问题是对的、对**读面/诊断**问题是错的。既有三口语义**一字未改**。
104
+ - **S-131 SQL 姿态读面从壳侧上收**(additive 六件 + 一个测试钩):`projectSqlEngineCapability` /
105
+ `SqlEngineReading` / `SqlEngineCapabilityView` / `observedSqlEngine` / `noteEngineCapsForSqlEngine` /
106
+ `forgetSqlEngineReading` / `sqlEngineDoctorDetail`。0.58.0 时它长在壳里的理由是**包侧结构上读不
107
+ 出来**(没有嵌套对象读口)—— 归层债,与上一条同批清。
108
+ - 🔴 **四态不是两态**:`unobserved`(本进程没观测过)/ `not_reported`(老引擎不报)/ `none`
109
+ (**引擎明确说没有 SQL 后端**,正面事实)/ `present`,四句 doctor 文案互不相同也互不蕴含。
110
+ - 🔴 `txnMode: null` 是**铸点域内的合法值**(上游逐字:`null` = 这个引擎没有这个指示位,**不是**
111
+ `optimistic`),`innodb`/`pg` 两种引擎恒 `null` —— 判成畸形 = B-025「消费域比铸点域更窄」复发。
112
+ - 🔴 **换代失效口是独立的一条路**:代际闸挡的是「旧响应写脏新一代」,这一格的病是反过来的
113
+ (新一代探测根本没成功 ⇒ tee 一次都没触发 ⇒ 上一台引擎的读数继续被当成当代事实答出去)。
114
+ - 🔴 **导出名与壳侧那份逐字同名 = drift-lock**(壳的同名影子导出门当场红,逼那份副本换装)。
115
+ 包内零 `process`:读锚走 `engineWireTarget()?.baseUrl`(其 env 臂读的就是同一个变量)。
116
+
117
+ ### 件③ 三件候件投影(cli [6476]⑥)
118
+
119
+ - **`tool_end.autoDenied`**(S-125⑧,server ≥7.57.0)进内部臂,**严格 `true` 才上臂**。
120
+ 🔴 它答的是既有两键答不了的那一问:`settledBy:"timeout"` + `resolution:"window_expired"` 覆盖
121
+ **一切**审批窗到期(普通 ask 的 TTL 也在内),分不出「自动拒」与「park 等人」—— 要渲
122
+ 「自动拒(限额回落)」而不是「已转后台候批」,判据只有这一位。缺席**禁**读成「是人拒的」。
123
+ - **`denialLimitFallback` 上卡**:见件①(端渲触限计数 + 哪道界 + 窗,并必须同呈「这一次得真人批」)。
124
+ - **`wiring_manifest.mcp[]`**(S-124 / core 7.5.0,server ≥7.60.0)—— `WiringManifestChromeEvent` 的
125
+ **第三段**,超集键 `_sema_mcp`(已登记 `docs/type-superset.json`)。
126
+ 🔴 **空数组不是缺席**:`mcp: []` = 「这条腿一台 MCP 都没申报」的**正面事实**,缺席才是「没报」;
127
+ 判在场写 `mcp !== undefined`,写 `mcp?.length` 就把两句话折成了一句。
128
+ ⚠️ **与同帧 `modelGate.removed: []` 规矩方向相反**(那一格空数组是坏值)—— 两条相反的规矩同处
129
+ 一帧,门里有一条**反向对照**钉着,不许被顺手统一。
130
+ 🔴 逐条独立(坏行只丢自己)· `toolCount: 0` 是读数不是缺席 · `errorCode` **不套闭集也不套形正则**
131
+ (十个具名词之外还有 `http_<status>` 形,词的属主在 core)· 远端作者的**自由文本 `error` 一格不投**。
132
+
133
+ ### 异源对抗复审 r1 两条 [medium] 全采(真病修)
134
+
135
+ - **「过滤后为空」不是「原本就是空」**(`projectMcpSection`)。一份非空却**零行幸存**的 `mcp`
136
+ (例:唯一那行缺 `status`)此前照样铸出 `[]` ⇒ 消费端按本臂义务④读到的是「这条腿一台都没申报」
137
+ 这句**肯定话**,一份读不出来的回体被伪装成一个确定的零。修 = 非空输入零行幸存 ⇒ **整段缺席**;
138
+ 只要还有幸存者,坏行照旧只丢自己。判据、判别力自证与边界(至少一行幸存)同批进门,
139
+ `seam.ts` 义务④、`type-superset` 条目与 §23c/黑盒判据 G5b 同步。
140
+ - **能力读点普查在对账前把该报的读点丢了**(`run-engine-caps-ledger-test.mjs`)。两条:
141
+ ① 读口表漏了 `engineCapState` ⇒ 经它的读点对本门隐形,台账可以一边写 `not_read` 一边真在读;
142
+ ② 收集时就按 SDK 锚过滤 ⇒ 一个**拼错的键名**(`permissionRulez`)连同它代表的真读点在对账
143
+ 之前蒸发,门原样绿,而产品上那条能力车道从此恒 `false`(权限治理入口永久隐形且不报错)。
144
+ 修 = 读口表补 `State`、键按**第 2 实参的位置事实**解析(不再「取参数表第一个串字面量」——
145
+ base 里的串会抢走它)、读口键**不预过滤**并新增 ④b「读口读的每个键都在 SDK 锚上」。
146
+ 两条复现出来的反例已长驻门内。
147
+
148
+ ### 门与登记
149
+
150
+ - 🆕 两道门 + 三处段级新增:`run-engine-caps-ledger-test.mjs`(新)· `run-sql-engine-capability-test.mjs`
151
+ (新)· `run-approval-frame-keys-test.mjs`(卡面腿)· `run-wiring-manifest-projection-test.mjs`(P6/A4)·
152
+ `run-additive-key-passthrough-test.mjs`(G6)· `run-hitl-gate-honesty-test.mjs`(F15)。
153
+ - 棘轮与登记同批:index 闭包 143→**144**(`sqlEngineCapability.ts`)· typeshape unknown 出境
154
+ 273→**277**(**四格逐格记账**)· 单例清单 281→**282** 条 / dupRisk high 94→**95**(逐条写下
155
+ 「复制两份会怎样」)· 公面导出基线 828→**836**(测试钩 41→**42**)· `docs/type-superset.json` 补
156
+ `_sema_mcp` 一行 · sdk 地板常量 8.2.0→**8.3.0**。
157
+
158
+ ## 0.58.0(2026-09-06)
159
+
160
+ > 内容批(2026-09-06,隔离树交付):三件 —— ①`@sema-agent/sdk` **8.2.0 提货**(peer 地板抬版,
161
+ > 本版**唯一**的非 additive 面)、②**B-025 修根**(「不再询问」候选窄读器不再手抄上游闭词表)、
162
+ > ③**L-102 下半场**(resume 拒绝**文案铸点**上收,壳侧改薄成适配层)。**本段只记内容**:
163
+ > `package.json.version` 未动、README Version 行未动、`FROZEN` 未动 —— bump 与段头转日期形归
164
+ > **发包批**(阶段一义务见本档头注)。
165
+ > 接入面详报见 `docs/INTEGRATION-CLIENTS.md` **§22**(三件各一小节 + 端的迁移动作 + 门表)。
166
+
167
+ **🔴 唯一的非 additive 面 = peer 地板抬到 `@sema-agent/sdk >=8.2.0`**(上一次是 0.48.0 的 `>=7.4.0`)。
168
+ 四条硬理由(逐条也写在 `scripts/run-sdk-floor-test.mjs` 的 `FLOOR` 注里,不是顺手跟版本):
169
+
170
+ 1. **三张 `as const` 闭词表是运行期判据** ⇒ 值级 import:`RULE_OFFER_MATCHES` /
171
+ `RULE_OFFER_BATCH_MEMBER_KINDS` / `RULE_OFFER_UNCOVERED_REASONS`(sdk 8.2.0 S-134 锚②)。
172
+ `hitl/toolApprovalWire.ts` 的窄读器直接读它们,<8.2.0 上这三个名字不存在,**整包编译不过**。
173
+ 这一条单独就足以钉死地板。
174
+ 2. `RuleOffer` / `RuleOfferMatch` / `RuleOfferBatchMember` / `RuleOfferUncoveredDetail` /
175
+ `RuleOfferUncoveredReason` **五个类型面**(同锚)—— 本包归一形现由它们派生。
176
+ 3. `ToolApprovalRespondAck` 上的 `persistedRule` / `persistedRules` / `persistedRuleAnchors` 三位 +
177
+ 导出的 `PersistedRuleAnchor`(S-134 锚③)—— 0.43.0 起的包内 additive 超集
178
+ `ToolApprovalRespondAckView` 按它**自己写下的退役条款**回收成 sdk 形的别名。
179
+ 4. `AgentEvent` 新增 `approval_revoke` 臂 —— 臂一进 union,`eventToSdkMessage` 的 B5 编译期穷举断言
180
+ `assertNeverArm` **当场红**(本批实翻:装上 8.2.0 的当拍 `tsc` 就是这一条错)。
181
+
182
+ 🔴 **同批亲验(别只信版本号)**:实装 8.2.0 的 `dist/resources/tool-approvals.js` 直读 ——
183
+ `RULE_OFFER_MATCHES` = `["exact","prefix","wildcard","subpath"]`(**四员**;⚠️ 不是 B-025 票面写的
184
+ "wildcard/glob")、`RULE_OFFER_BATCH_MEMBER_KINDS` = `["command","directoryRead"]`、
185
+ `RULE_OFFER_UNCOVERED_REASONS` = `["redirection","no_rule_form","cap_overflow"]`、
186
+ `TOOL_APPROVAL_FRAME_KEYS` **18 项且含 `ruleOffers`**。
187
+
188
+ ---
189
+
190
+ ### 件① sdk 8.2.0 提货(锚追平 / 两笔到期记账退役 / 一条新臂进 switch)
191
+
192
+ - **`AHEAD_OF_ANCHOR` 的 `ruleOffers` 领先登记退役**(`run-approval-frame-keys-test.mjs`)——
193
+ 这是**登记自己的退出条件到期**,不是清理:sdk 8.2.0 的运行期锚已含该键 ⇒ 门先**自红一次**
194
+ (红词逐字「领先登记 ruleOffers:SDK 锚**仍无**此键 —— 锚补上了就该删这条登记」)再删,与
195
+ `persistedRuleShadowed`(#229)/ `probeCause`(#280 件1)两次走同一条路。
196
+ ⚠️ **同批不顺手退别的**:`requiresRealApproval` / 窗三键 / `inputHasBidi` / `parked` 在 8.2.0 的锚上
197
+ **仍然没有**(同一次 node 直读)—— 退出条件是**逐条**的,不是整表的,表回空才是健康态。
198
+ - **`ToolApprovalRespondAckView` 与 `PersistedRuleAnchor` 回收成 sdk 形的别名**(名字保留 ⇒ 端源码
199
+ 兼容,读法逐字不变)。判形与窄读**一个字节没改**:`readToolApprovalRespondAck` 的每一条纪律照旧
200
+ (三位规范文本回显整只判形 / 单复数同场即矛盾 / 回显在场而 `rulePersisted !== true` 即矛盾 /
201
+ 锚与清单逐位置同文本)—— 上游补的是**类型**,不是判官。
202
+ - **`approval_revoke` 臂进 switch**,处置 = `dropped/unsupported_arm`(**有痕**,经
203
+ `reportDroppedFrame` 走宿主 sink),与兄弟 `approval_request` 同姿势、同理由:本包今天对新族两帧
204
+ **零消费口**(`isToolApprovalFrame` 只认 legacy 两词),而撤卡帧带的是用户可见的状态迁移,
205
+ 悄悄丢掉比吼一行更坏。缺口如实记账,接那条新卡链是另一批。
206
+ - **§7d P-44 转「已收」**:sdk 8.1.0 那一族型面缺口在 8.2.0 全部补齐。🔴 本包侧**本来就零 cast**
207
+ (P-44 原文已写清:本包不铸 respond 的 wire 体,吃缺口的是三端**注入面**)⇒ 剩余动作在三端 ——
208
+ 注入面那处宽 `cast` 可以删掉,改用 sdk 8.2.0 的 `PersistRuleSelection`(两臂互斥 `?: never` 钉在编译期)。
209
+
210
+ ### 件② B-025 修根:窄读器不再手抄上游闭词表(病形 = 手抄的判据跟不上单源)
211
+
212
+ **病形**(0.43.0 铸、0.57.0 仍在;由 sdk [6464] 两侧源码对读点名,坐标
213
+ `src/hitl/toolApprovalWire.ts:1772` / `:209`,cli [6465] 认账立 B-025):
214
+
215
+ | 症状 | 0.57.0 实际行为 | 用户看到什么 |
216
+ |---|---|---|
217
+ | ① `match` 词表**手抄**成两员 `'exact' \| 'prefix'`,而 core design/382 / #510 已扩到四员 | 一条合法的 `match:"wildcard"`(或 `"subpath"`)single **整条丢** | 卡上那一格「不再询问」**凭空消失** |
218
+ | ② batch 成员**手抄**成「三元组 + segment」,没有 `directoryRead` 臂 | 一只带 `cd` 段目录只读授权的 batch **整只丢** | 同上;且与该函数自己注释承诺的「逐条丢坏、不整拒」相反 |
219
+ | ③ 没有 `uncoveredDetail` 座 | design/382 §3.5 的 additive 明细整段读不到 | 「为什么这段还会问」无从渲 |
220
+
221
+ **修**(不是补两个词,是**取消抄本**):三张闭词表单源化到 sdk 8.2.0,本包再导出同一份数组对象
222
+ (端拿它判定,不再手抄字面量);`RuleOfferMatch` / `RuleOfferBatchMember` /
223
+ `RuleOfferUncoveredDetail` / `RuleOfferUncoveredReason` 四形改由 sdk 派生。
224
+
225
+ - 🔴 **`match` 词集双向全等钉**(写进门):⊇ 表里**每个**词的 single 都必须被读器认下(手抄的窄表
226
+ 在新词上当场红,那正是症状①);⊆ 构造出来**保证在表外**的词必须被丢(读器放宽成「是串就收」
227
+ 时当场红)。退役键 `ruleSuggestions` 那条归一臂共用同一把三位窄化 ⇒ 新词表在那条腿上同样生效
228
+ (族扫:病形不许只修当格)。
229
+ - 🔴 **成员 `kind` 不识 ⇒ 丢整只 batch,绝不丢单个成员,也绝不丢整卡** —— 这是 core design/382 §2.3
230
+ 的**规范性降级臂**(逐字 "drops the WHOLE batch offer — never the single member ... and never the
231
+ whole card";sdk 8.2.0 顶注同文,上游发布帖 ④2 的黑盒判据亦同)。⚠️ **B-025 票面「成员级容错」
232
+ 那句与三处上游原文相反,本批按上游原文实现并把它钉成正向断言**(直接影响:`directoryRead` 因为
233
+ **被认回**而整只留下,而不是因为「容错」被拆散)——理由见 §20c 第 3 条。
234
+ - ⚠️ **另收一条 pre-B3 兼容臂**:`kind` **缺席**的成员按 `kind:'command'` 归一(server ≥7.46.0 到
235
+ design/382 §2.3 B3 落地之间铸的成员没有判别位;窄读域只许等于或宽于铸点域)。归一形上 `kind`
236
+ **恒在场**,端拿到的成员永远是判别联合,零分支差异。
237
+ - **additive `uncoveredDetail` 透传**:🔴 **缺席 ≠「没有未覆盖段」**(server 对坏形的座只丢座不丢批,
238
+ 老引擎压根不铸;真源恒是 `uncoveredSegments` 那个 count)⇒ 缺席时**不铸空数组**;坏行**只丢那一行**,
239
+ 坏载体只让**本位**降缺席,**绝不**因为一个 additive 位否决整只 batch;行数与 count **不强制相等**。
240
+
241
+ **异源对抗复审 r1 两条 [medium] 全采(真病,均在本批修掉)**:
242
+ - 🔴 **`uncoveredDetail` 行数帽 8 → 32(比铸点域更窄的窄读域,[4050] 禁的形)**:初稿按「与成员帽
243
+ 同源同量级」类比出一个 8,而那是**类比不是取证**、且判错了源 —— server 的执法帽是
244
+ `MAX_UNCOVERED_DETAIL_ROWS = 32`,且**未覆盖段数根本不受成员帽约束**(core 逐字
245
+ `uncoveredSegments: uncoveredDetail.length`,一只**只有 1 个成员**的 batch 可以带 9 条明细:
246
+ 9 个带重定向的段各自 mint 不出规则形,进不了 `rules[]` 却每段都要给一个「为什么还会问」)。
247
+ 后果 = 一只合法的 9~32 行明细座被整座静默丢掉,人失去「这几段为什么还会问」。取**等于**铸点域
248
+ 而不是再自行加倍(与 `MAX_RULE_OFFERS_TOLERATED` 不同:那一位没有可锚的上游常量,余量是纯容忍;
249
+ 本位有具名铸点常量,再加倍就是又一次凭感觉编数)。边界钉进门 ⑨d:9 / 32 必收、33 才丢。
250
+ - 🔴 **成员 `kind` 的判据本身还是手抄的**(同形残余,异源对抗复审的负控质疑逼出来的):初稿把
251
+ `'command'` / `'directoryRead'` 两个词**手写在分支条件里**,导入的 `RULE_OFFER_BATCH_MEMBER_KINDS`
252
+ 只被再导出、运行期一次都没读 —— 那正是 B-025 本身的病形(`match` 手抄两员)在成员这一维上的
253
+ 残余,只是当天恰好抄对了。修 = 成员性判到闭词表 + 臂分派兜底(**表内但本包还没有读器**的 kind
254
+ 与表外同处置:丢 ⇒ 整只 batch 丢,fail toward asking;而门 ⑨c 的 ⊇ 腿会当天红逼人补读器)。
255
+ 同批把该条负控从「缺字段的坏成员」换成「**完全合形、只有 kind 在表外**」——否则 `readRuleTriple`
256
+ 会替 kind 判据把它拒掉,负控量不到该量的东西。
257
+ ⚠️ 诚实记录:修完之后产品侧有**两道**判据一起挡它,**只删表检查**是一次**行为不变**的变异(门照绿
258
+ 是正确的);把两道**一起**削回初稿结构才是真回归形状,门当场红(收货亲跑实证:该变异下读器返回
259
+ `["single@0","batch@1"]`,坏批被放行)。
260
+
261
+ **⚠️ 端要跟的型面差分**(本件唯一会让端编译红的地方,而红是要的):`RuleOfferMatch` 从两员变四员 ⇒
262
+ 对它写穷尽 `switch` 的端会在 `wildcard` / `subpath` 两格红。那两格此前在端上是**静默不可达**的
263
+ (包在读器里就丢掉了),不是「新增的空格子」。`RuleOfferBatchMember` 变判别联合 ⇒ 渲成员前先读 `kind`。
264
+
265
+ **同形存量族扫**(病形 = 「本包手抄上游 `as const` 闭词表」):sdk 8.2.0 一共导出 5 张这样的表,
266
+ 另两张(`CONTROL_ROLES` / `NOT_FOUND_TASK_OWNER_CODES`)在本包 `src/**` **零命中**(亲跑 grep,
267
+ 含各自的成员字面量)⇒ 本族**只有规则报价这三张**,已全部单源化,无残余。
268
+
269
+ ### 件③ L-102 下半场:resume 拒绝**文案铸点**上收(新公面 `src/resumeRefusalCopy.ts`)
270
+
271
+ 0.57.0 上收的是**事实读数**(§21 `resumeRetryLaterFromError`);留在壳里的另一半是**人话** ——
272
+ cli 1.0.x `src/sema/resumeRefusalCopy.ts` 自铸了一份判型 + 三句 `·` 分段文案,而 web-client /
273
+ desktop 要么没有、要么将来会再抄一份。三端各抄一份 = 同一次拒绝在三个端上说三句不一样的话,
274
+ 而这三句回答的是**同一个安全问题**:「这次拒绝有没有消费掉我的决定 / 这张卡还能不能再决」。
275
+
276
+ **新增公面 7 件**(公面导出 **821 → 828**):`RESUME_REFUSAL_CODES` · `resumeRefusalFromError` ·
277
+ `resumeRefusalContent` · `RESUME_PLACEMENT_MISMATCH`(码常量,`engineErrorCodes.ts`)+ 件② 的三张闭词表再导出。
278
+
279
+ - 🔴 **两个闭集刻意分家**:`RESUME_RETRY_LATER_CODES`(§21)闭的是「**server 在哪些码上铸
280
+ `retryAfterSec`**」⇒ 答「能不能等」;`RESUME_REFUSAL_CODES`(本件)闭的是「**哪些 resume 拒绝有
281
+ 人话可补**」⇒ 答「该对人说什么」。两集**交于** `resume.preflight_rejected` 一码、**各有**一个独占
282
+ 成员 —— `usage_window_exhausted` 只在前者(没有专属人话,「你可以等」就是全部信息);
283
+ `placement_mismatch` 只在后者(server 不给它铸窗,而且**等一会儿对它毫无用处**:出路是换 root,
284
+ 是**换参数**不是**等时间**;放进时间性闭集 = 让人白等一个永远不会自己好的拒绝)。
285
+ - 🔴 **不复制判定**:交点码的窗与可等性一律**转调** `resumeRetryLaterFromError`,本模块里没有第二个
286
+ `retryAfterSec` 窄读器(`paired-mechanisms-must-share-premise`:两处各判必然在某一格分叉)。
287
+ 门用九形入参驱动两口,`waitable` 与 `retryAfterSec` 必须**逐字相同**(含「缺席」那一格)。
288
+ - 🔴 **文案第三句由 `waitable` 选,绝不由 message 文本选**(按上游散文分支正是「上游改一个词、端的
289
+ 判定静默空转」的形):`placement_mismatch` ⇒ 说出路(换 root);`preflight_rejected` **无窗** ⇒
290
+ **不可判**,说两条臂都成立的那句、把「还能不能再赎」交给屏上那行引擎原文(cli 1.0.x 现行文案逐字);
291
+ **有窗** ⇒ 那是 retry_later 臂的**充分证据**(core 的 terminal 臂抛的 `CheckpointError` 一个 detail
292
+ 都不带,窗结构上到不了客户端)⇒ 才敢说「障碍清掉之后这个 token 还能再赎」。
293
+ - **闭集是 `Object.freeze` 数组**(与 `RESUME_RETRY_LATER_CODES` 同一条已定谳的病形):公开面与判定源
294
+ 是同一个物,`ReadonlySet` 只在类型面只读,一次 `.add` 就能把「重开吧」挂上安全声明。
295
+ - **文案里不铸秒数**:等待那一行由端单独渲(壳的 `retryAfterHint` 是那一位的唯一取值口),
296
+ 写进文案 = 同一个数字上屏两遍。
297
+
298
+ **⚠️ 壳提货后的行为差分(是修好不是回归)**:`retryAfterSec` 窄读域从 cli 自铸的「有限数 ∧ ≥0」
299
+ 收到与 §21 同律的「**整数 ∧ ≥1**」。server 的铸键逐字是「ms → 秒**向上取整**、**下限 1**」⇒ 真供给里
300
+ 不存在 0 / 负数 / 小数;放行 `0` 就是对端说「立刻重试」,而 resume 是 **AT-MOST-ONCE** 的有副作用动作
301
+ (叫醒 = 真跑一轮)—— 一个 0 会把「等一会儿」变成热循环。
302
+
303
+ **可达性如实登记**(不许把预置说成到货):两码在 server 7.51 当版**结构性不可达**(四条腿都不供
304
+ `placementRoot`,也没有 `resumePreflight` 座)。本模块与壳那份一样是**预置臂**,收进包是为了
305
+ 「码到货那天三端不至于各说各话」,不是在声称这条指路已经上过屏。
306
+
307
+ ---
308
+
309
+ ### 常驻门
310
+
311
+ - 🆕 `scripts/run-resume-refusal-copy-test.mjs`(**103 checks**):公面在场 · 两个闭集关系**双向钉** ·
312
+ 不复制判定(九形入参两口逐字相同)· `placement_mismatch` 恒无窗恒不可等**且带窗也不翻、那个窗也不透传** ·
313
+ 窄读域「整数 ∧ ≥1」· 文案三句由 `waitable` 选而非 message(两族原话灌进 message 而文案一字不动)·
314
+ 负控(同族六码 + 429 同名不同门 + 下划线两族 + 别族 + 裸 404 + 码在 message 里被引用 + 六形坏载体)·
315
+ 结构读不 instanceof · 退役 `code` 槽两码各走一遍都不认 · 纯函数。
316
+ - `scripts/run-rule-offers-reader-test.mjs`:**109 → 169 checks**(新增段 ⑨ 六小节,判据见 §22d)。
317
+ - `scripts/run-approval-frame-keys-test.mjs`:`ruleOffers` 领先登记退役后回到逐元素相等
318
+ (SDK 锚 18 项 / 本仓镜像 24 项 / 领先登记 6 项)。
319
+ - `scripts/run-sdk-floor-test.mjs`:`FLOOR` 8.2.0 与 peer 声明**三位逐位等值**互绑。
320
+ - `scripts/run-public-surface-test.mjs`:公面导出基线 **821 → 828**;peer 地板 + README 三处逐字核。
321
+
322
+ ### 已知局限(本版新增)
323
+
324
+ - **`approval_revoke` / `approval_request` 两帧本包仍零消费口**:处置是有痕的
325
+ `dropped/unsupported_arm`。端不要把「撤卡帧没反应」读成「引擎没发」;接那条新卡链是另一批。
326
+ - **`run-approval-frame-keys-test.mjs` 的 fixture 腿在 server ≥7.57 上红**(本批**非引入方**,
327
+ 实测 7.57/7.58 缺 `ruleOffersAbsence`、7.59 起再缺 `denialLimitFallback`、7.60 起再缺 `origin`)——
328
+ 这是另一条已知病形(「镜像滞后于 server 实发键」)的存量,与本批的病形(「手抄闭词表」)不同族,
329
+ 按坐标上报另立。本批 `npm test` 走的是 SDK 锚腿(fixture 腿在本仓默认不跑,`@sema-agent/server`
330
+ 不是本包的 dep)。
331
+
52
332
  ## 0.57.0(2026-09-05)
53
333
 
54
334
  > 内容批 D(2026-09-05,隔离树交付):三件 —— L-103/[C228] 窄读器公面化、L-102 resume 时间性拒绝
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.57.0
38
+ **Version:** 0.59.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
@@ -63,9 +63,11 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
63
63
  it ships no runtime code at all.
64
64
  - `@sema-agent/sdk` — the wire contract. **Value-level, not type-only**: `AgentClient`,
65
65
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
66
- five modules, and the browser bundle really bundles the SDK through (the portability guard would
66
+ five modules, and so are the three frozen rule-offer vocabularies the approval reader narrows
67
+ against — the tables live upstream precisely so this package does not keep a second copy that can
68
+ fall behind. The browser bundle really bundles the SDK through (the portability guard would
67
69
  exit 3 rather than quietly mark it external).
68
- - The declared floor is `>=7.4.0`, and it is *witnessed*: the guard checks that an actually
70
+ - The declared floor is `>=8.3.0`, and it is *witnessed*: the guard checks that an actually
69
71
  installed SDK at that line still exports every value-level symbol this package imports and still
70
72
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
71
73
  promise, not a contract.
@@ -232,6 +234,8 @@ public-surface guard checks that last one).
232
234
  | `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
233
235
  | `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
234
236
  | `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
237
+ | `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code |
238
+ | `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but carries no such key (an older engine), the engine explicitly answered `null` — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock |
235
239
  | `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
236
240
  | `scripts/run-limits-env-failloud-test.mjs` | `SEMA_HEADLESS_*` env-lane limits reject invalid values as loudly as the flag lane (no silent "no budget" runs) |
237
241
  | `scripts/run-streamjson-timing-honesty-test.mjs` | Stream timing & terminal honesty ([2084]): held errored fs-write results release on model progress; a wall-clock stop maps to `error_during_execution` with a truthful salvage note; the synthetic API-error assistant row carries the `<synthetic>` in-message sentinel. Also ([2489], core 5.8.0): the run-limit `errorCode` -> CC subtype map is pinned code by code (`limits.max_{cost,turns,tokens,walltime}_exceeded`), token/wall-clock stops keep the text the engine already produced, and the `failed` event arm shares the one mapping point. The 5.7 dual-vocabulary legs retired with server 6.0.0 (which bundles core 5.8.0); four **retirement negative controls** stand in their place — the retired `status:'timeout'` and the retired codes must fall to the honest fallback subtype and must never drop back to an empty success, so putting any of them back turns the gate red |
@@ -265,7 +269,8 @@ public-surface guard checks that last one).
265
269
  | `scripts/run-esc-halt-plan-test.mjs` | The Esc stop decision every client shares: fire the **turn-level** halt first, and escalate to a **run-level** cancel in exactly two cases — the engine itself answered with a 409 from the closed code set (it is saying "there is no in-flight turn here; use cancel for a run-level stop"), or that shot came back with no verdict at all *and* the shell can independently prove a permission card was on screen. Everything else does not escalate. The asymmetry is the whole point and every negative control guards the same direction — deciding *not* to escalate costs the user one more choice on a busy-session card (recoverable), deciding to escalate wrongly tears down a run that was alive and takes every in-flight tool with it (not). So: the closed code set is a **frozen** value, not a `ReadonlySet` — type-level immutability does not stop a consumer's `.add()`, and the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift; the escalation gate is the **conjunction** of that closed set and the 409 status, since honouring the code alone lets a 500 that merely quotes it drive a destructive call; `interrupt.not_held` and `steering.not_running` are deliberately outside the set (the first means *this replica* has no live face — the run may be perfectly alive on another); an unreadable code falls to the no-escalation side; a `parked` flag never overrides a verdict the engine did give, and only strict `true` counts when it did not. The first shot is unconditional by construction — it does not consult `parked`, because the 409 it earns is exactly the verdict the gate wants — and the verdict itself is a closed machine-readable reason word, not display copy |
266
270
  | `scripts/run-peer-frame-projection-test.mjs` | The three engine-injected lanes design/385 puts on the **one** `task_notification` carrier, which are not the same kind of thing at all: a delegated child's uplink (`agentMessage`), another session's message drained from this session's own box (`crossSessionMessage`), and a receipt about one of *this* session's own outbound messages (`crossSessionNotice`). The engine renders none of them inside a `<task-notification>` shell, so a client that projects them as the generic completion card shows "background task finished" while the model read a colleague's sentence — two faces describing different events. The discriminator is pinned to the **typed carrier being present**, never to the `summary` text: those carriers can only be minted by the engine's injection legs (the external `notify()` input is a strict subset of the payload and can wear none of them), while `summary` is filled by every notification there is — so anchoring on text would let any background task impersonate a colleague's message by writing `<agent-message from="…">` into its own summary, and a positive control asserts exactly that payload still projects as the generic card. Fail-closed has two tiers rather than one: a broken **required** field (empty `from`, a non-string `body`, a notice `kind` outside the closed set) returns absence so the caller falls back to the generic card — an honest downgrade where the user still sees the notification — while a broken **optional** field drops only itself, because losing an attribution note and losing a colleague's whole message are not the same magnitude. The provenance side record is **required and must agree on four points** (`kind` matches the lane; `from`/`taskId`/`seq` are present and equal the carrier/payload — each equality is anchored on a core mint site and pinned by the cli wire-anchor A-K24), so a carrier signed with a trusted name but a disagreeing provenance falls back to the generic card; peer bodies pass the same authority-envelope neutralization core applies (`<task-notification>` etc. are defused) so a colleague's text can never seed the resume dedup ledger. Lane precedence copies the engine renderer's own order, because the model already read the frame in that order and a client ordering of its own would put a card on screen that disagrees with the frame the model saw. Rendering and parsing of the transcript line live in the same module and are round-tripped in both directions, including a body carrying a forged closing tag (a parser fooled there hands half a message to the next row) and a quote inside the sender label (which must not forge a second attribute); the notice lane is deliberately kept **out** of the parser, since recognising it would mean anchoring the `[Cross-session …]` prefix and a user typing that same line would be rendered as engine speech. Hostile carriers are read as own **data** descriptors only and accessors are never invoked at all — `catch` catches throwing, not never returning — proven by a counting getter that must stay at zero calls, alongside a revoked proxy and a prototype-only carrier; and four legacy payload shapes assert the no-carrier path is byte-identical to before, which is the executable form of "zero difference for an older host" |
267
271
  | `scripts/run-wiring-manifest-projection-test.mjs` | The two end-user facts carried on the engine's `wiring_manifest` frame (`modelGate`: which tools this run's model gate removed and the verbatim restore hint; `autoMode`: whether auto mode is actually armed and the engine's own reason word). Projection: both sections ride as `_sema_`-prefixed superset keys, verbatim, and no SDK-named key is minted; a frame where neither section is well-formed projects to `none/not_in_slice` (no empty arm); `modelGate` needs all three keys and treats `removed: []` as a bad value rather than a reading; `autoMode` needs a boolean plus a non-empty reason that agrees with it, and the reason word is never mapped onto the capabilities vocabulary; the frame is flat (a nested `manifest:{}` wrapper is not a supply); `eventId` rides like every other arm. Adapter: exactly one chrome event on the main lane, a sub-flow frame (any `parentToolCallId`, `null` included) yields nothing, and an absent `eventId` leaves the key absent. Added at receiving time because the shell-side gate could not see this package's behaviour: two mutations (empty `removed` accepted, sub-flow gate removed) had passed the package suite untouched |
268
- | `scripts/run-rule-offers-reader-test.mjs` | The narrowing reader behind the "don't ask again" options, now a public entry point rather than a card-port-only one. Hosts that render the frame themselves (a browser has no three-way terminal card) previously had to rebuild this reader on their side, and what it carries is a **redemption-safety** judgement, not a convenience: the batch arm is redeemed by **index**, so a reader that compacts the array after dropping a malformed entry makes the k-th option a person clicked and the k-th rule the server writes two different rules. So: a bad entry is dropped **on its own** (one bad option must not make a real one disappear) while every surviving entry keeps its **original wire index** — pinned from both ends, with the bad entries leading and trailing. A batch's *members* are the opposite: any malformed member drops the whole batch, because a conjunctive batch is one "yes" to all of them and a batch missing a member is a different grant; its honest-remainder count is a reading, not decoration, so a non-integer or negative value drops the batch rather than rendering a fabricated zero. An empty array, a non-array, an over-cap array and an all-bad array all read as **absence** rather than an empty list, because an empty list renders as "there is an option lane with nothing in it". The two wire generations are ordered by a rule, not a preference: the newer key wins outright, a newer key that is **present but unreadable** does not fall back to the retired key (borrowing the older material would pass someone else's options off as this request's), and a `null` newer key reads as absence so a relaying layer that serialises "missing" as null cannot delete the whole lane on older engines. The public entry is finally reconciled against **both** card-port legs on the same material, byte for byte, so the exported reader and the one the card sees can never become two |
272
+ | `scripts/run-rule-offers-reader-test.mjs` | The narrowing reader behind the "don't ask again" options, now a public entry point rather than a card-port-only one. Hosts that render the frame themselves (a browser has no three-way terminal card) previously had to rebuild this reader on their side, and what it carries is a **redemption-safety** judgement, not a convenience: the batch arm is redeemed by **index**, so a reader that compacts the array after dropping a malformed entry makes the k-th option a person clicked and the k-th rule the server writes two different rules. So: a bad entry is dropped **on its own** (one bad option must not make a real one disappear) while every surviving entry keeps its **original wire index** — pinned from both ends, with the bad entries leading and trailing. A batch's *members* are the opposite: any malformed member drops the whole batch, because a conjunctive batch is one "yes" to all of them and a batch missing a member is a different grant; its honest-remainder count is a reading, not decoration, so a non-integer or negative value drops the batch rather than rendering a fabricated zero. An empty array, a non-array, an over-cap array and an all-bad array all read as **absence** rather than an empty list, because an empty list renders as "there is an option lane with nothing in it". The two wire generations are ordered by a rule, not a preference: the newer key wins outright, a newer key that is **present but unreadable** does not fall back to the retired key (borrowing the older material would pass someone else's options off as this request's), and a `null` newer key reads as absence so a relaying layer that serialises "missing" as null cannot delete the whole lane on older engines. The public entry is finally reconciled against **both** card-port legs on the same material, byte for byte, so the exported reader and the one the card sees can never become two. Two upstream vocabularies used to be **hand-copied** here, and both had fallen behind: a match word outside the copied pair dropped an otherwise valid option outright, and a batch carrying a directory-read member — a member kind the copy did not know — dropped the whole batch. Both tables now come from one place upstream and are re-exported verbatim, pinned in both directions: every word in the table must be accepted (a narrower copy reds on the words it never learned) and a word constructed to be outside it must still be refused (a reader widened to "any string" reds too), with the retired-key normalising leg sharing the same narrowing so the fix cannot land on one leg only. A member whose kind is genuinely unknown still drops **the whole batch and only that batch** — never one member, because a conjunctive batch one member short renders "yes to N" as "yes to N−1", and never the card, because the honest single beside it is intact — while a member from before the discriminant existed normalises to the historical kind rather than being refused. The additive per-segment reasons ride through verbatim, drop only the row that is malformed, and stay **absent rather than empty** when nothing survives, since an empty list would read as "confirmed nothing uncovered" while the count remains the only source of truth |
273
+ | `scripts/run-resume-refusal-copy-test.mjs` | The **words** a client says when a resume is refused, minted once here instead of three times. The facts behind them already lived in this package; the sentences did not, so each client wrote its own — and those sentences answer a safety question (was my decision consumed, can this token still be redeemed), which is exactly the kind of answer that must not vary by client. Two closed sets meet here and the guard pins their relationship in both directions, because it is a premise rather than a coincidence: one set answers *can waiting help* (the codes the server mints a wait on), the other answers *what should a person be told*, they **intersect in exactly one code**, and each keeps a member the other must not have — a placement mismatch is never waitable no matter what arrives on the response, since its remedy is a changed argument rather than elapsed time, and a full governance window needs no prose because "you can wait" is the whole message. The overlapping code delegates its wait and its disposition to the existing reading rather than judging again: nine shapes of input drive both entry points and the two readings must agree byte for byte, the absent case included, because two judges always diverge somewhere. The wait is narrowed to the domain the server mints it in, which is **stricter than the shell's own copy was** — a zero now reads as no window rather than as "retry now", and the wake-up it would retry is an at-most-once action with real side effects. The third sentence is chosen by the disposition, never by the engine's prose: rewriting the message to either upstream branch's exact wording, with the window untouched, must leave all three sentences unchanged, while adding a window must change the third one and only the third one |
269
274
  | `scripts/run-resume-retry-later-test.mjs` | The two resume refusals that carry a **wait quantity** — the only members of that refusal family that do, which is the whole reason they form a closed set. Carrying a wait is not the same as being the only ones worth waiting on: a sibling refusal in the same family clears on its own and the engine says so in words, it just cannot put a number on it, so *not recognised here* must never be read as *waiting will not help*. One of the two also has a *terminal* upstream branch that arrives under the same code with the distinguishing detail only in prose, so recognition alone is not permission to say "try again": the disposition is decided by **positive evidence** and pinned from both directions — the quota-window code is evidence in itself, the preflight code counts only when the server really supplied a wait (an upstream fact, not a convention: the terminal branch throws with no detail at all, so a wait value cannot reach the client on that path), and a preflight refusal with no wait reads as *undecidable* (say what is true of both branches — nothing was consumed — and leave redeemability to the engine's own line) rather than being rendered as either a retry or an ending. Every other member means waiting will not help (change a setting, relaunch, the retained session is gone), so the recognition is a **closed set of two codes**: widening it to a family prefix would tell half the users to wait and the other half to keep waiting for something that will never arrive, and the negative controls drive exactly those codes through it, plus a same-named code on a different door (the submission-side quota refusal), the two underscore-form siblings, and a code merely quoted inside a message body. The wait value is narrowed to the same domain the server mints it in (a whole number of seconds, at least one): zero, a negative, a fraction and a non-number all read as **no window given** rather than as zero, because a zero tells the caller to retry immediately and the wake-up it would retry is an at-most-once action with real side effects. Reading is structural rather than `instanceof`, since the client is host-injected and the same class name across two bundles is two classes, and a null-prototype plain object must still be recognised. The failure classifier gains this one disposition without any existing one moving, an unknown code still falls to the honest open-set arm and its wait value is **not** believed, and an end-to-end call proves the disposition and the window reach the host while the call itself is still attempted exactly once. The recognised code set is a **frozen array**, not a type-level readonly set: the latter is a plain mutable collection at runtime and the decision reads the same instance, so one `.add` from any consumer would turn a refusal that waiting cannot fix into one that claims it can — the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift |
270
275
 
271
276
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
@@ -314,15 +314,20 @@ const wiringManifestArm = function* (m) {
314
314
  return;
315
315
  const modelGate = m._sema_modelGate;
316
316
  const autoMode = m._sema_autoMode;
317
+ const mcp = m._sema_mcp;
317
318
  const hasGate = typeof modelGate === 'object' && modelGate !== null;
318
319
  const hasAuto = typeof autoMode === 'object' && autoMode !== null;
319
- if (!hasGate && !hasAuto)
320
+ // S-124(0.59.0):🔴 `Array.isArray` 判在场,**不判 `.length`** —— `mcp: []` 是「这条腿一台都
321
+ // 没申报」这句正面事实,按长度判会把它和「老 mint 没报」折成同一件事(core 顶注点名的读法)。
322
+ const hasMcp = Array.isArray(mcp);
323
+ if (!hasGate && !hasAuto && !hasMcp)
320
324
  return;
321
325
  yield chrome({
322
326
  kind: 'wiring_manifest',
323
327
  laneProof: MAIN,
324
328
  ...(hasGate ? { modelGate: modelGate } : {}),
325
329
  ...(hasAuto ? { autoMode: autoMode } : {}),
330
+ ...(hasMcp ? { mcp: mcp } : {}),
326
331
  ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
327
332
  });
328
333
  };
@@ -170,6 +170,33 @@ export interface WiringManifestAutoMode {
170
170
  /** core **六词逐字透传**。🔴 不映射 `/v1/capabilities.permissionModeAuto.reason` —— 见投影函数头注。 */
171
171
  reason: string;
172
172
  }
173
+ /**
174
+ * {@link wiringManifestSupersetBody} 的 `mcp[]` 一条目(S-124 / core 7.5.0,server ≥7.60.0 的形)。
175
+ *
176
+ * 「我这条腿**申报**的一台 MCP 服务器,连上了没有、挂了是哪一类、挂上来几个工具」。
177
+ *
178
+ * 🔴 **`name` + `status` 是必需两座**(与 server 侧 `wiringManifestEventData` 的 `mcp` 臂同判据:
179
+ * `if (name === undefined || status === undefined) continue`)—— 一条既没有名字也没有状态的行
180
+ * 渲不出任何有用的话,而**坏一条只丢那一条**,绝不丢整张表(丢整表 = 用户以为「一台都没申报」)。
181
+ * 🔴 **`errorCode` 不是纯闭集**:core `MCP_FAILURE_CODES` 十个具名词之外还有 `http_<status>` 形
182
+ * (core 自陈)⇒ 本包**既不枚举那十个词、也不套形正则**(词的单一属主在 core;抄一份会把 core
183
+ * 加的新词与新形一起吞成缺席),只判非空串。消费端按具名词写的 `switch` **必须带 default 臂**。
184
+ * 🔴 **`error` 自由文本上游就不投,本包更不铸**:它是这条面上唯一的**远端作者自由文本**,server
185
+ * 侧的裁定逐字是「进来会同时破本段『无自由文本 ⇒ 不脱敏』的段内不变量,和 core 把远端 MCP 错误
186
+ * 文本脱敏收敛成**一个**铸点的单铸律」。可操作的因由在 `errorCode`。
187
+ */
188
+ export interface WiringManifestMcpEntry {
189
+ /** spec 声明名(引擎产的标识,非用户内容)。 */
190
+ name: string;
191
+ /** 三词闭集(core 属主;**按开集读**,认不得的词照渲不丢行)。 */
192
+ status: string;
193
+ /** 声明方自述的层级标签(core 已 single-line + 长度封顶后逐字回显)。缺席 = 没报。 */
194
+ source?: string;
195
+ /** 失败因由的**机器码**(十词 + `http_<status>` 形,开集)。缺席 = 这条没失败,或引擎没给码。 */
196
+ errorCode?: string;
197
+ /** 这台服务器挂上来的工具数(有限数)。缺席 = 没报,**不是** `0`。 */
198
+ toolCount?: number;
199
+ }
173
200
  /**
174
201
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
175
202
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
@@ -269,6 +269,23 @@ export function eventToSdkMessage(ev, ctx) {
269
269
  // 直拒、以及 durable/decide 腿的结算(它们带自己的 settledBy/reason)都**不带**这一位。
270
270
  // 缺席 ≠「不是拒绝」也 ≠「人拒的」⇒ 缺席就不铸键(绝不折成 null / 空串)。
271
271
  const toolEndResolution = ev.resolution;
272
+ // ── S-125⑧(0.59.0):`autoDenied` —— 这次 deny **是引擎的窗自己拒的**,不是人拒的 ──────
273
+ // 出处 = server ≥7.57.0 `trace/project.js` 的 `toolEndEventData`(条件 stamp
274
+ // `ev.autoDenied === true ? { autoDenied: true } : {}`),源头是 core 7.4.0 auto 模式
275
+ // 分类器的**限额回落窗**:那张必须真人批的卡(`tool_approval.denialLimitFallback` +
276
+ // `requiresRealApproval` 孪生键)在 `autoDenyAfterMs` 走完之后由**引擎**判 deny,同批盖
277
+ // `settledBy:"timeout"` / `resolution:"window_expired"` / 本位。
278
+ // 🔴 **为什么单靠既有两键判不出来**(这一位非补不可的理由):`settledBy:"timeout"` 覆盖的是
279
+ // **一切**审批窗到期(含普通 ask 的 TTL),`resolution:"window_expired"` 同理 —— 两者都
280
+ // 答不了「这一次到期是**自动拒**收的场,还是 park 了等人」。消费面要渲的那句话
281
+ // (「自动拒(限额回落)」vs「已转后台候批」)正卡在这一格上。
282
+ // 🔴 **严格 `true` 才上臂**(与 `_sema_collateral_abort` / `governanceForced` 同族纪律):
283
+ // 机读位是二值的,「在场但不是 true」没有语义,折成在场会让消费端把一个坏形当成肯定。
284
+ // 🔴 **缺席不带语义,不许反推**:缺席同时覆盖「不是自动拒」「老引擎(<7.57.0)不报」
285
+ // 「这次根本没走审批」三形 ⇒ 缺席**禁**读成「是人拒的」。
286
+ // 🔴 **不据它自铸第二只定时器/第二张资格表**:窗的执行全在引擎(上游 `denialLimitFallback`
287
+ // 顶注的同一条禁令),本位只是**事后**的判别位。
288
+ const toolEndAutoDenied = ev.autoDenied;
272
289
  // ── #324 半场(0.40.0):连坐 abort 机读位 `_sema_collateral_abort`([4907] 裁定)──
273
290
  // 出处**不是引擎**,是本包自己的 HITL 桥:`gateLedger.flushHeld` 对「出身 = collateral」的
274
291
  // 扣留帧 stamp(见 `SEMA_COLLATERAL_ABORT_KEY` 头注)。桥在本臂**上游**(宿主先用
@@ -309,6 +326,8 @@ export function eventToSdkMessage(ev, ctx) {
309
326
  ...(typeof toolEndResolution === 'string' && toolEndResolution.length > 0
310
327
  ? { resolution: toolEndResolution }
311
328
  : {}),
329
+ // S-125⑧:自动拒判别位(**严格 true** 才上臂;见上方头注)。
330
+ ...(toolEndAutoDenied === true ? { autoDenied: true } : {}),
312
331
  // #324:连坐 abort 机读位(严格 true 才上臂;见上方头注)。
313
332
  ...(collateralAbort === true ? { _sema_collateral_abort: true } : {}),
314
333
  ...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
@@ -721,6 +740,25 @@ export function eventToSdkMessage(ev, ctx) {
721
740
  */
722
741
  case 'approval_request':
723
742
  return dropped('unsupported_arm', 'approval_request');
743
+ /**
744
+ * `approval_revoke`(design/172 撤卡帧;server ≥7.55.0 起也进重放,SDK 8.x 才把它写进
745
+ * `AgentEvent` union —— 0.58.0 sdk 8.2.0 提货批**编译期逼进来**的臂,与 `engine_notice`
746
+ * / `human_input` 走的是同一条路:臂一进 union,`default` 的 B5 穷举断言当场红)。
747
+ *
748
+ * 🔴 **与 `approval_request` 同源同处置,所以并**在它旁边而不是并进上面那两条 —— 它是
749
+ * 同一条流内审批协议的**撤销**半场(载荷同是 `ApprovalFrameEnvelope`,`schemaVersion`
750
+ * 未知的帧要按信封先读再窄化)。本包今天**零消费口**:`isToolApprovalFrame` 只认
751
+ * legacy 的 `tool_approval` / `tool_approval_complete` 两词,新族两帧一个都不接。
752
+ * 🔴 分类 = `dropped/unsupported_arm` 而**不是** `nothing/not_in_slice`(与
753
+ * `approval_request` 同一条理由,逐字见上一臂):`none` 是静默的,只有 `dropped` 会经
754
+ * `reportDroppedFrame` 走宿主 sink + console 留痕。撤卡帧带的是**用户可见的状态迁移**
755
+ * (那张卡已经不该再被决断了),悄悄丢掉它比吼一行更坏。
756
+ * 📋 缺口如实记账(**不**在本批修 —— 与 `approval_request` 是同一条新卡链,属功能不属修复):
757
+ * 接法在上一臂的「接法」段里写着,撤卡半场按 `approvalId` 撤同一张卡;在那条链落地之前
758
+ * **不许**由本切片顺手编一个假 transcript 形来假装接上了。
759
+ */
760
+ case 'approval_revoke':
761
+ return dropped('unsupported_arm', 'approval_revoke');
724
762
  // ── `engine_notice`(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)────
725
763
  // 🔴 **到期复核已兑现(sdk 7.4.0 提货,0.48.0)** —— 与 `human_input`(sdk 6.9.0)逐字同一条路。
726
764
  // 本臂此前是 switch **之前**的一条 raw 预分派,理由 = 它还没进已发布 SDK 的 `AgentEvent`
@@ -811,6 +849,49 @@ function textEndProjection(ev, ctx) {
811
849
  : {}),
812
850
  })));
813
851
  }
852
+ /**
853
+ * `wiring_manifest.mcp` 的形校验(S-124 / core 7.5.0,server ≥7.60.0)。
854
+ *
855
+ * 🔴 **空数组 ≠ 缺席,两者都要能被消费端分辨**(core 顶注逐字:"an empty array is *this leg
856
+ * declared no servers*, absence is an older mint or an external derivation")⇒ **真的**空数组
857
+ * 原样铸成 `[]`。把它折成缺席 = 把「我一台都没申报」这句**正面事实**说成「不知道」。
858
+ * 🔴 **但「过滤后为空」不是「原本就是空」**(异源对抗复审 r1 [medium] 采纳,真病):一份
859
+ * `[{name:'github', errorCode:'http_503'}]`(缺 `status`)进来时,唯一那一行被下面的逐行判据
860
+ * 丢掉 —— 若照样铸出 `[]`,消费端按本臂的义务④读到的是**「这条腿一台都没申报」这句肯定话**,
861
+ * 一份读不懂的回体就此被伪装成一个确定的零申报。⇒ **非空输入而零行幸存 ⇒ 整段缺席**
862
+ * (「我读不出来」不是「我知道是零」)。这与下面「逐条独立」并不矛盾:只要**还有幸存者**,
863
+ * 半张表仍是真读数,坏行照旧只丢自己。
864
+ * 🔴 **逐条独立**:一条坏行只丢自己(与 server 侧 `continue` 同判据),绝不因为一条坏行丢整表。
865
+ * 🔴 **逐键挑,禁 spread**:core 往条目上加第六键时必须由人显式处置 —— 而它下一个加的很可能又是
866
+ * 一个像 `error` 那样的自由文本面。
867
+ * ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
868
+ * 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
869
+ */
870
+ function projectMcpSection(raw) {
871
+ if (!Array.isArray(raw))
872
+ return undefined;
873
+ const rows = [];
874
+ for (const e of raw) {
875
+ if (typeof e !== 'object' || e === null || Array.isArray(e))
876
+ continue;
877
+ const r = e;
878
+ const name = typeof r.name === 'string' && r.name.length > 0 ? r.name : undefined;
879
+ const status = typeof r.status === 'string' && r.status.length > 0 ? r.status : undefined;
880
+ if (name === undefined || status === undefined)
881
+ continue;
882
+ rows.push({
883
+ name,
884
+ status,
885
+ ...(typeof r.source === 'string' && r.source.length > 0 ? { source: r.source } : {}),
886
+ ...(typeof r.errorCode === 'string' && r.errorCode.length > 0 ? { errorCode: r.errorCode } : {}),
887
+ ...(typeof r.toolCount === 'number' && Number.isFinite(r.toolCount) ? { toolCount: r.toolCount } : {}),
888
+ });
889
+ }
890
+ // 🔴 见顶注:`raw.length === 0` 是**正面事实**(照铸 `[]`);非空却零行幸存 = 读不出来 ⇒ 段缺席。
891
+ if (raw.length > 0 && rows.length === 0)
892
+ return undefined;
893
+ return rows;
894
+ }
814
895
  /**
815
896
  * `wiring_manifest.modelGate` 的形校验(core #524 / server S-86,server ≥7.58.0)。
816
897
  *
@@ -879,11 +960,16 @@ function projectAutoModeSection(raw) {
879
960
  function wiringManifestSupersetBody(ev) {
880
961
  const modelGate = projectModelGateSection(ev.modelGate);
881
962
  const autoMode = projectAutoModeSection(ev.autoMode);
882
- if (modelGate === undefined && autoMode === undefined)
963
+ // S-124(0.59.0):第三段。🔴 判「三段都不在场」用的是 `mcp === undefined`(**不是** `.length`)——
964
+ // 空数组是一句正面事实(「这条腿一台都没申报」),拿它当「没有这一段」会让 `mcp: []` 的帧连同
965
+ // 另两段一起落回 `not_in_slice`,而那正是 core 顶注点名要防的读法。
966
+ const mcp = projectMcpSection(ev.mcp);
967
+ if (modelGate === undefined && autoMode === undefined && mcp === undefined)
883
968
  return undefined;
884
969
  return {
885
970
  ...(modelGate !== undefined ? { _sema_modelGate: modelGate } : {}),
886
971
  ...(autoMode !== undefined ? { _sema_autoMode: autoMode } : {}),
972
+ ...(mcp !== undefined ? { _sema_mcp: mcp } : {}),
887
973
  };
888
974
  }
889
975
  function engineNoticeProjection(ev, ctx) {
@@ -82,6 +82,46 @@ export declare function engineCapTrue(baseUrl: string | undefined, key: string):
82
82
  * 要分辨成因请自己读 caps 对象;本口只答放行问题。
83
83
  */
84
84
  export declare function engineCapNestedTrue(baseUrl: string | undefined, parentKey: string, key: string): boolean;
85
+ /**
86
+ * 一格能力位上「**有没有值**」的四态(0.59.0,S-131 请托的通用半场)。
87
+ *
88
+ * 🔴 为什么必须是四态而不是「有 / 没有」:`/v1/capabilities` 上有**至少三种**不同的「读不出来」,
89
+ * 它们对运维是三条不同的下一步,折成一件就是把「不知道」渲成一个答案
90
+ * ([honest-absence-not-fabricated-zero]):
91
+ * · `unobserved` —— 这个 base 的 caps **一次都没落地**(没 kick / 还在飞 / 探测失败)。
92
+ * **不是**「引擎没有这个位」;
93
+ * · `not_reported` —— caps 在手,回体上**没有**这个键 ⇒ 老引擎。**不是**「这台部署没接」;
94
+ * · `null` —— 引擎**明确说** `null`。这是一个**正面事实**(上游用 `null` 表达「这台部署确实
95
+ * 没有这个设施」的位就走这一档),把它折进上面两档等于把引擎真给的答案丢掉;
96
+ * · `value` —— 有值(对象 / 数组 / 串 / 数 / 布尔,原样带出)。
97
+ * 🔴 **`value` 不做任何形校验**:形归各自的窄读器(单点铸判据),通用口再校一遍就会在上游 additive
98
+ * 加成员那天把一个合法值挡在包边界外,还让「谁把它窄没的」在排障时说不清。
99
+ */
100
+ export type EngineCapValueState = {
101
+ kind: 'unobserved';
102
+ } | {
103
+ kind: 'not_reported';
104
+ } | {
105
+ kind: 'null';
106
+ } | {
107
+ kind: 'value';
108
+ value: unknown;
109
+ };
110
+ /**
111
+ * 同步读口(**任意值 / 嵌套对象**):该 base 的 caps 里 key 上到底有没有值,四态诚实作答。
112
+ *
113
+ * 为什么要有它:`engineCapTrue` / `engineCapNestedTrue` / `engineCapString` 三口的返回域分别是
114
+ * `boolean` / `boolean` / `string|undefined` —— 它们**把四种「读不出」全折成 fail-closed 的一档**,
115
+ * 那对**放行**问题是对的(能力位缺席就别走那条车道),但对**读面/诊断**问题是错的:一格
116
+ * `sql: {engine,isolation,txnMode} | null`(server ≥7.60.0)用这三口读,`null`(这台部署确实没库)
117
+ * 与「老引擎不报」与「还没探到」在屏上会变成同一句话。S-131 那次因此在**壳里**长出了一份窄读器
118
+ * (归层债),本口是它在包侧的座位。
119
+ *
120
+ * 🔴 本口只答「有没有值」,**不答「值成不成形」** —— 后者归各能力位自己的窄读器
121
+ * (例:`sql` 的 {@link import("./sqlEngineCapability.js").projectSqlEngineCapability})。
122
+ * 两层刻意分开:通用口不认识任何一个具体位的形,窄读器不该重新实现一遍缓存与代际。
123
+ */
124
+ export declare function engineCapValue(baseUrl: string | undefined, key: string): EngineCapValueState;
85
125
  /**
86
126
  * 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
87
127
  *