@sema-agent/client-core 0.42.0 → 0.43.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.
@@ -87,6 +87,56 @@ export declare function surfaceRememberNotApplied(): void;
87
87
  * 已记进 docs/refactor/README.md 的宿主/上游工单表,本层不做旁路补偿(只做如实告知)。
88
88
  */
89
89
  export declare function surfaceEditNotForwarded(): void;
90
+ /**
91
+ * 文案(测试锁字面)。措辞刻意描述**后果**,并且**只说证得出的话**(异源对抗复审四轮 [medium] 修):
92
+ * · 不写「这台引擎不支持」——能力位缺席有三种同形成因(宿主没接这个字段 / 探测还没回来 / 探测失败),
93
+ * 其中只有明确的 `false` 才勉强算「引擎说了不」。写成断言就是替引擎宣布一件没证据的事
94
+ * ([honest-absence-not-fabricated-zero]);改成 `could not be confirmed` 的未知口径。
95
+ * · 「审批本身过了」这句**只在 respond 真成功之后**才成立 —— 所以本通知的**发出时机**被移到
96
+ * `await respond(...)` 成功之后(见 `toolApprovalWire` 的调用点顶注),而不是丢键那一刻。
97
+ */
98
+ export declare const RULE_NOT_SENT_WARN_TEXT = "your \"don't ask again\" choice was NOT sent to this engine \u2014 support for that rule form could not be confirmed. The approval itself went through, but you will be asked again";
99
+ /**
100
+ * 卡上选中了「不再询问」(编辑臂的自由文本 / 批臂的合取批),而**这台引擎的能力位没有确认**
101
+ * 那条兑付通道 ⇒ 编排层把持久臂整条丢掉、只送决断。
102
+ *
103
+ * 🔴 为什么这必须上屏而不是只记 debug(异源对抗复审三轮 [high] 采纳,0.43.0):它与
104
+ * {@link surfaceRememberNotApplied} 是**同一个病**——用户按下了一个明确的意图,系统把它静默
105
+ * 丢掉,然后下一次照旧弹卡。用户能得出的唯一结论是「这个功能坏了」或「我按错了」。卡上那一格
106
+ * 是**真 affordance**(offer 确实在场、确实可以走本地落规则那条路),被丢的只是**这条 wire
107
+ * 兑付通道** —— 所以正解不是把选项藏起来,是**如实说出来**。
108
+ * 🔴 **一条通知服务两条臂**(同形清剿):编辑臂(`respondFreeFormRules` 位缺席)与批臂
109
+ * (`respondBatchRuleOffers` 位缺席)此前都只写 debug —— 同一个病形两处存量,同批一起改。
110
+ * 🔴 决断本身**不受影响**(照常送达),所以文案第二句说清「审批过了,只是规则没存」——
111
+ * ⚠️ 正因为文案里有那句话,**调用时机必须在 `respond` 真成功之后**(异源对抗复审四轮 [medium]:
112
+ * 丢键那一刻就发,而随后 respond 抛错 ⇒ 决断其实**没有**落定,用户却已经读到「审批过了」,
113
+ * 可能按「工具已经在跑」继续操作)。调用点的时序契约见 `toolApprovalWire` 那一处。
114
+ * 🔴 文案**不说「引擎不支持」**:能力位缺席三种同形成因(宿主没接字段 / 探测未回 / 探测失败),
115
+ * 断言引擎的能力是没有证据的话 —— 用 `could not be confirmed` 的未知口径。
116
+ * 无宿主口(print/headless 没屏)⇒ 静默,与本文件其余 surface 同语义。
117
+ */
118
+ export declare function surfaceRuleArmNotSent(): void;
119
+ /**
120
+ * 文案(测试锁字面)。与 {@link RULE_NOT_SENT_WARN_TEXT} **刻意分两条**:两者对用户的下一步建议不同 ——
121
+ * 那条说「这台引擎能不能收还不知道」(换台引擎/等探测就好了),本条说「你这次的选择**本身**没过
122
+ * 客户端那道核对」(卡口实现有问题或同时给了两个互斥选择,换台引擎也不会变)。折成一条会让任一方
123
+ * 谎报原因。措辞同样只说证得出的话:不猜是哪一种坏法,把两种都摆出来。
124
+ */
125
+ export declare const RULE_NOT_SENT_REJECTED_WARN_TEXT = "your \"don't ask again\" choice was NOT sent \u2014 it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again";
126
+ /**
127
+ * 卡上选中了「不再询问」,而那次选择**没过包内那道表核 / 互斥核**(报了引擎没 offer 过的文本、
128
+ * 下标不指向 batch offer、或文本臂与批臂同场)⇒ 编排层把持久臂整条丢掉、只送决断。
129
+ *
130
+ * 🔴 为什么这也必须上屏(异源对抗复审十轮 [medium] 采纳,0.43.0):它与
131
+ * {@link surfaceRuleArmNotSent} 是**同一个诚实性问题**,只是原因换了一个 —— 用户按下的明确意图
132
+ * 被系统丢掉,审批照常成功,下次照旧弹卡。此前这三条丢键路径**只写 `hostLog('error')`**:
133
+ * 那是给开发者看的诊断,**不是**给按下按钮的那个人的答复。
134
+ * 🔴 **时序契约与那条一致**:文案里有「审批本身过了」⇒ 只在 `await respond(...)` 真成功之后发。
135
+ * 🔴 **成因通常是卡口实现的 bug**(表外文本/坏下标/两臂同场都是宿主侧构造出来的),所以 `hostLog`
136
+ * 的那条 `error` 留痕**照旧保留** —— 两个受众,两条通道,谁都不顶替谁。
137
+ * 无宿主口(print/headless 没屏)⇒ 静默,与本文件其余 surface 同语义。
138
+ */
139
+ export declare function surfaceRuleArmRejected(): void;
90
140
  /**
91
141
  * 中断 deny 的有界观察(壳侧单测 `hitlCancelDeny.test.ts` 的被测面;REF-CC-023 起两条决断腿共用)。
92
142
  *
@@ -163,6 +163,65 @@ export function surfaceRememberNotApplied() {
163
163
  export function surfaceEditNotForwarded() {
164
164
  surfaceSelfClearingWarn(EDIT_NOT_FORWARDED_KEY, EDIT_NOT_FORWARDED_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
165
165
  }
166
+ /**
167
+ * 文案(测试锁字面)。措辞刻意描述**后果**,并且**只说证得出的话**(异源对抗复审四轮 [medium] 修):
168
+ * · 不写「这台引擎不支持」——能力位缺席有三种同形成因(宿主没接这个字段 / 探测还没回来 / 探测失败),
169
+ * 其中只有明确的 `false` 才勉强算「引擎说了不」。写成断言就是替引擎宣布一件没证据的事
170
+ * ([honest-absence-not-fabricated-zero]);改成 `could not be confirmed` 的未知口径。
171
+ * · 「审批本身过了」这句**只在 respond 真成功之后**才成立 —— 所以本通知的**发出时机**被移到
172
+ * `await respond(...)` 成功之后(见 `toolApprovalWire` 的调用点顶注),而不是丢键那一刻。
173
+ */
174
+ export const RULE_NOT_SENT_WARN_TEXT = 'your "don\'t ask again" choice was NOT sent to this engine — support for that rule form could not be confirmed. The approval itself went through, but you will be asked again';
175
+ const RULE_NOT_SENT_KEY = 'hitl-rule-arm-not-sent';
176
+ /** 与 remember 那条同量级(都是「你以为记住了、其实没有」),取同一个时长。 */
177
+ const RULE_NOT_SENT_TIMEOUT_MS = 10_000;
178
+ /**
179
+ * 卡上选中了「不再询问」(编辑臂的自由文本 / 批臂的合取批),而**这台引擎的能力位没有确认**
180
+ * 那条兑付通道 ⇒ 编排层把持久臂整条丢掉、只送决断。
181
+ *
182
+ * 🔴 为什么这必须上屏而不是只记 debug(异源对抗复审三轮 [high] 采纳,0.43.0):它与
183
+ * {@link surfaceRememberNotApplied} 是**同一个病**——用户按下了一个明确的意图,系统把它静默
184
+ * 丢掉,然后下一次照旧弹卡。用户能得出的唯一结论是「这个功能坏了」或「我按错了」。卡上那一格
185
+ * 是**真 affordance**(offer 确实在场、确实可以走本地落规则那条路),被丢的只是**这条 wire
186
+ * 兑付通道** —— 所以正解不是把选项藏起来,是**如实说出来**。
187
+ * 🔴 **一条通知服务两条臂**(同形清剿):编辑臂(`respondFreeFormRules` 位缺席)与批臂
188
+ * (`respondBatchRuleOffers` 位缺席)此前都只写 debug —— 同一个病形两处存量,同批一起改。
189
+ * 🔴 决断本身**不受影响**(照常送达),所以文案第二句说清「审批过了,只是规则没存」——
190
+ * ⚠️ 正因为文案里有那句话,**调用时机必须在 `respond` 真成功之后**(异源对抗复审四轮 [medium]:
191
+ * 丢键那一刻就发,而随后 respond 抛错 ⇒ 决断其实**没有**落定,用户却已经读到「审批过了」,
192
+ * 可能按「工具已经在跑」继续操作)。调用点的时序契约见 `toolApprovalWire` 那一处。
193
+ * 🔴 文案**不说「引擎不支持」**:能力位缺席三种同形成因(宿主没接字段 / 探测未回 / 探测失败),
194
+ * 断言引擎的能力是没有证据的话 —— 用 `could not be confirmed` 的未知口径。
195
+ * 无宿主口(print/headless 没屏)⇒ 静默,与本文件其余 surface 同语义。
196
+ */
197
+ export function surfaceRuleArmNotSent() {
198
+ surfaceSelfClearingWarn(RULE_NOT_SENT_KEY, RULE_NOT_SENT_WARN_TEXT, RULE_NOT_SENT_TIMEOUT_MS);
199
+ }
200
+ /**
201
+ * 文案(测试锁字面)。与 {@link RULE_NOT_SENT_WARN_TEXT} **刻意分两条**:两者对用户的下一步建议不同 ——
202
+ * 那条说「这台引擎能不能收还不知道」(换台引擎/等探测就好了),本条说「你这次的选择**本身**没过
203
+ * 客户端那道核对」(卡口实现有问题或同时给了两个互斥选择,换台引擎也不会变)。折成一条会让任一方
204
+ * 谎报原因。措辞同样只说证得出的话:不猜是哪一种坏法,把两种都摆出来。
205
+ */
206
+ export const RULE_NOT_SENT_REJECTED_WARN_TEXT = 'your "don\'t ask again" choice was NOT sent — it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again';
207
+ const RULE_NOT_SENT_REJECTED_KEY = 'hitl-rule-arm-rejected';
208
+ const RULE_NOT_SENT_REJECTED_TIMEOUT_MS = 10_000;
209
+ /**
210
+ * 卡上选中了「不再询问」,而那次选择**没过包内那道表核 / 互斥核**(报了引擎没 offer 过的文本、
211
+ * 下标不指向 batch offer、或文本臂与批臂同场)⇒ 编排层把持久臂整条丢掉、只送决断。
212
+ *
213
+ * 🔴 为什么这也必须上屏(异源对抗复审十轮 [medium] 采纳,0.43.0):它与
214
+ * {@link surfaceRuleArmNotSent} 是**同一个诚实性问题**,只是原因换了一个 —— 用户按下的明确意图
215
+ * 被系统丢掉,审批照常成功,下次照旧弹卡。此前这三条丢键路径**只写 `hostLog('error')`**:
216
+ * 那是给开发者看的诊断,**不是**给按下按钮的那个人的答复。
217
+ * 🔴 **时序契约与那条一致**:文案里有「审批本身过了」⇒ 只在 `await respond(...)` 真成功之后发。
218
+ * 🔴 **成因通常是卡口实现的 bug**(表外文本/坏下标/两臂同场都是宿主侧构造出来的),所以 `hostLog`
219
+ * 的那条 `error` 留痕**照旧保留** —— 两个受众,两条通道,谁都不顶替谁。
220
+ * 无宿主口(print/headless 没屏)⇒ 静默,与本文件其余 surface 同语义。
221
+ */
222
+ export function surfaceRuleArmRejected() {
223
+ surfaceSelfClearingWarn(RULE_NOT_SENT_REJECTED_KEY, RULE_NOT_SENT_REJECTED_WARN_TEXT, RULE_NOT_SENT_REJECTED_TIMEOUT_MS);
224
+ }
166
225
  /**
167
226
  * cancel-by-deny 的「良性 no_pending」判据。**结构化 `.code` 为主,`instanceof` 只作加强,绝不替代**:
168
227
  * - `.code` 是 client-core 自家 `HitlSafetyError` 的契约属性(hitlBridge.ts,REF-CC-036 起是闭集
@@ -3,7 +3,7 @@ import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverla
3
3
  import { hostLog } from '../host.js';
4
4
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
5
5
  import { observeCancelByDeny } from './hitlHostSurface.js';
6
- import { isAskTool } from './frameRouter.js';
6
+ import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
7
7
  import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
8
8
  /** 一 turn 内最多循环这么多次 park(防御:引擎/模型病态连环提问时不无限 attach)。 */
9
9
  const MAX_GATE_HOPS = 24;
@@ -286,7 +286,19 @@ export async function resolvePark(park, ctx) {
286
286
  }
287
287
  if (outcome.kind !== 'decided') {
288
288
  hostLog('debug', `liveHitlAskWire: gate not decided (${outcome.kind}${'reason' in outcome ? `: ${outcome.reason}` : ''}) — fail-soft to suspended terminal`);
289
- const events = [...led.flushHeld()]; // 回退:毒化帧照旧渲染(= 修复前的诚实红)
289
+ // 回退:毒化帧照旧渲染(= 修复前的诚实红)
290
+ // 件 B(异源复审 finding 采纳):**五个排水出口一律走同一个中断感知出口** —— 本出口今天恒是
291
+ // park 批(走到这里的前提就是有一张 park),零-park 硬门必挡,所以是**语义等价的 no-op**;
292
+ // 写成统一形是为了「新开一个出口就绕过改写」这条病形从此在结构上不成立(常驻门:hitl F13-h
293
+ // 钉住 src 下 `flushHeld()` 的调用点恰好一处)。
294
+ const events = [
295
+ ...flushHeldWithInterruptRewrite(led, {
296
+ terminal: park.pendingDone,
297
+ signal: ctx.signal,
298
+ // 模 B:`aborted` = 用户在门卡上按了 Esc(该 outcome 在包内的语义就是逐字这一条)。
299
+ gateAbortedByUser: outcome.kind === 'aborted',
300
+ }),
301
+ ];
290
302
  if (park.pendingDone) {
291
303
  events.push(park.pendingDone);
292
304
  }
@@ -122,6 +122,88 @@ export interface FsApprovalWireDeps {
122
122
  /** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
123
123
  * 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
124
124
  export declare function structurallyEqual(a: unknown, b: unknown): boolean;
125
+ /**
126
+ * 一条「不再询问」候选的匹配形(server/core 闭词表,两代同词)。
127
+ */
128
+ export type RuleOfferMatch = 'exact' | 'prefix';
129
+ /**
130
+ * `batch` 臂的一个成员 —— 复合命令**某一段**的规则(core `SegmentRuleSuggestion` 逐字)。
131
+ * `segment` = 这条成员是从哪一段折叠后的命令文本铸出来的:**渲染座,永不参与裁决**;
132
+ * 它与 `rule`/`command` 同属 **UNTRUSTED-for-display**(原始 post-rewrite 命令字节)。
133
+ * 上游形与本包归一形**共用这一个**(成员上没有下标这回事,两代/两腿逐字相同)。
134
+ */
135
+ export interface RuleOfferBatchMember {
136
+ rule: string;
137
+ match: RuleOfferMatch;
138
+ command: string;
139
+ segment: string;
140
+ }
141
+ /**
142
+ * **wire 上的**一条 offer —— 与上游 core `RuleOffer` 逐字同形(**没有** `offerIndex`)。
143
+ *
144
+ * 🔴 为什么与归一形 {@link RuleOffer} 分开声明(异源对抗复审 [medium] 采纳):`offerIndex` 是
145
+ * **本包铸的座**(见 {@link RuleOffer} 顶注),把它写进 {@link ToolApprovalFrame.ruleOffers} 的
146
+ * 入参型 = 要求宿主为一个上游根本不发的字段负责 —— 真实 7.46.0 帧会**类型不合**,宿主只能强转,
147
+ * 或者自己伪造一个本该由窄读器按**当前这条腿**算出来的下标(而两条腿的下标语义还不一样)。
148
+ * ⇒ 入参面(帧/行)用本形,出参面(卡入参)用带下标的归一形,两者刻意不互相赋值。
149
+ */
150
+ export type WireRuleOffer = {
151
+ kind: 'single';
152
+ rule: string;
153
+ match: RuleOfferMatch;
154
+ command: string;
155
+ } | {
156
+ kind: 'batch';
157
+ rules: readonly RuleOfferBatchMember[];
158
+ uncoveredSegments: number;
159
+ };
160
+ /**
161
+ * 一张审批卡上的**一个**「不再询问」选项(core 5.58.0 design/375 §3.1 的判别联合,server ≥7.46.0
162
+ * 起在 `tool_approval` 帧 / `card_json` / durable park 行**三腿同形**发出)。
163
+ *
164
+ * ── 为什么本包自铸这个形(记账,不是偷懒)────────────────────────────────────────────────
165
+ * 上游 wire 形的类型供给口是 `@sema-agent/sdk`,而 sdk 7.2.0(今天 npm 上的最新版)的
166
+ * `ToolApprovalFrame` / `PendingCheckpoint` 上**只有退役键 `ruleSuggestions`**、没有 `RuleOffer`
167
+ * 这个名字 —— 与 `requiresRealApproval`(#283)/ 窗三键([4851])同形:**server 已真发、SDK 锚未跟**。
168
+ * 照那两例的先例:先追 server,不等 SDK;键集侧进对账门的 `AHEAD_OF_ANCHOR` 带退出条件登记
169
+ * (SDK 补上当天自红逼删)。语义来源锚 = engine 7.46.0 fixture 的
170
+ * `@sema-agent/core/dist/core/permission-rule-model.d.ts`(`RuleOffer` 的逐字契约)+
171
+ * `@sema-agent/server/dist/approval-card.d.ts`(两臂 zod 形与两条腿的下标语义)。
172
+ *
173
+ * ── `offerIndex` 是本包铸的座(上游形上没有这一位)──────────────────────────────────────
174
+ * core 的契约原话:消费端丢掉不认识的成员时**必须保住每个留下来成员的原始 wire 下标**,做不到就
175
+ * 「must suppress its persistence actions entirely (fail toward asking)」—— 因为 `batch` 臂的兑付
176
+ * 键就是**下标**(`persistRule.batchOfferIndex`)。本包的窄读器是**逐条丢坏的**(一条坏 offer 不该
177
+ * 让另一条真 offer 消失,与 server `boundedRuleOffers` 同向),压紧就会让下标前移 ⇒ 人点的第 k 个
178
+ * 与服务端兑的第 k 个指向两条不同规则。所以窄读时把**原始下标**显式记在这一位上,兑付时按它回报。
179
+ * 🔴 `offerIndex` 的定义域 = 「本包在**这条腿上**收到的那个数组里的下标」:
180
+ * · **活卡帧腿**(`ToolApprovalFrame.ruleOffers`):server 同步腿是纯前缀截、零逐条丢弃 ⇒ 它恒等于
181
+ * core 的 offer index,**是**合法选择键,`surfaceToolApprovalFrameAndRespond` 按它回兑;
182
+ * · **durable 行腿**(`PendingCheckpoint.ruleOffers` → {@link ApprovalCardRequest.ruleOffersReadOnly}):
183
+ * server `boundedRuleOffers` 已经逐条丢弃并压紧过一次 ⇒ 行上的下标本就不是 core 的 offer index。
184
+ * 这条腿**根本没有兑付口**(`/decide` 体无规则位),所以那一位只是展示/对账座,**禁**当选择键。
185
+ */
186
+ export type RuleOffer = {
187
+ kind: 'single';
188
+ /** 见本联合顶注:原始 wire 下标(活卡帧腿=选择键;durable 腿=展示座)。 */
189
+ offerIndex: number;
190
+ /** 宿主要兑付的规范规则文本(ENGINE 铸,客户端只能原样回报)。 */
191
+ rule: string;
192
+ match: RuleOfferMatch;
193
+ /** 规则里的命令模式,给不想重解析的宿主直接渲。UNTRUSTED-for-display。 */
194
+ command: string;
195
+ } | {
196
+ kind: 'batch';
197
+ /** 见本联合顶注。**batch 臂的兑付就是按这个数**(文本不可抄:合取批没有单条文本)。 */
198
+ offerIndex: number;
199
+ /** 1..5 条逐段规则(段序,按规则文本去重)。勾这一项 = 对**全体成员**一次性说是,
200
+ * 合取批**没有成员级选中**(要更窄的答案就选 single 臂或只批这一次)。 */
201
+ rules: RuleOfferBatchMember[];
202
+ /** 铸卡时刻的诚实余量披露:这批兑完之后,按当时的覆盖快照仍未被任何规则放行的段数。
203
+ * 0 = 「兑完这批,这条复合命令在那份快照的口径下就全覆盖了」。它是**对铸卡快照的陈述**,
204
+ * 不是长期保证(并发删规则会让它过时)。 */
205
+ uncoveredSegments: number;
206
+ };
125
207
  /** 三选卡的原始决断(vendor 卡的三个选项 + abort + 表面失败)。allow_session=第 2 项
126
208
  * (vendor 产出非空 permissionUpdates:setMode acceptEdits / cwd 外 addDirectories)。 */
127
209
  /** allow 决断臂(命名形,typeshape B4 口径;0.26.0 因 persistRule 位达 4 成员抽名)。 */
@@ -151,6 +233,29 @@ export interface ApprovalCardAllowDecision {
151
233
  * 未确认 ⇒ 编排层**整条丢 persistRule**(决断照送),见该位注。
152
234
  */
153
235
  persistRuleEdited?: true;
236
+ /**
237
+ * #334 批臂(0.43.0;server ≥7.46.0 `ParsedPersistRule` 的 `{kind:'batch', batchOfferIndex}` 那一支,
238
+ * design/377):人在卡上勾的是一条 **`kind:'batch'` 的合取批 offer**。
239
+ *
240
+ * 值 = 那条 offer 的 {@link RuleOffer.offerIndex}(**原始 wire 下标**)。
241
+ * 🔴 **为什么批臂传下标而不是文本**:合取批没有单条可抄的文本(它是 1..5 条规则的一次性授权),
242
+ * 文本等式在这一臂上不存在 ⇒ 选择键只能是下标。single 臂的防伪锚仍是**文本等式**,下标
243
+ * **不许**旁路它(server 侧同款:`batchOfferIndex` 窄开只指 batch)。
244
+ * 🔴 **三臂互斥**(server `PERSIST_RULE_BATCH_EXCLUSIVE_ERROR`,同场**响亮 400 且连决断一起拒**):
245
+ * 本位与 {@link persistRule}/{@link persistRuleEdited} 同场 ⇒ 编排层**整条持久臂丢弃 + 留痕**,
246
+ * 决断照送。两臂说的是两次不同的授权,替人挑一个 = 替人改主意;而让 400 把决断一起打掉,
247
+ * 是把附带愿望的失败升级成主动作的失败(方向与 note 超限那条同一纪律)。
248
+ * 🔴 **表核不让位**:本位只在「该下标在**本帧**的 offers 里真是一条 `kind:'batch'`」时上 wire ——
249
+ * 越界 / 指向 single / 负数 / 非整数一律丢键留痕(server 会 400,包边界先挡下来,决断不受连累)。
250
+ * 🔴 **必须由宿主供给能力位**(异源对抗复审四轮 [medium] 订正 —— 本段初稿写的是「批臂供给自闸、
251
+ * 没有独立能力位」,与实装**相反**,而这段注释随 `.d.ts` 出包 = 三端读到的接入契约):
252
+ * 编排层只在 {@link ToolApprovalFrameLaneOpts.respondBatchRuleOffersCapable} `=== true` 时把本位
253
+ * 送上 wire。**宿主不供给这一位 ⇒ 现代引擎上每一次批臂勾选都会被丢**(决断照送 + 一条如实通知)。
254
+ * 理由见那一位的顶注:帧上有 batch offer 只证明**出帧端**认识新形,不证明**收 respond 的那台**
255
+ * 认识它(滚动升级 / 多副本 / 代理),而旧解析器对本键是**响亮 400 且连决断一起拒**。
256
+ * 缺席 = 这次不是批臂(候选臂/编辑臂/不兑付),现状字节不变。
257
+ */
258
+ persistRuleBatchOfferIndex?: number;
154
259
  }
155
260
  /**
156
261
  * deny 决断臂(命名形,typeshape B4 口径;0.28.0 因 `reason` 位抽名,与 0.26.0
@@ -230,26 +335,56 @@ export interface ApprovalCardRequest {
230
335
  * `agentName` UNTRUSTED-for-display。子代 ask 才在场。 */
231
336
  delegation?: ToolApprovalDelegation;
232
337
  /**
233
- * 持久规则候选(#225 件1,原样来自 {@link ToolApprovalFrame.ruleSuggestions} 的合形项)——
234
- * 卡据此渲「不再询问」档;缺席/空 = 卡形与 0.25.0 字节不变(不渲该档)。
235
- * 🔴 卡只许把其中一条的 `rule` **原样**放进决断的 `persistRule`(不拼、不改写);
236
- * 出身裁剪(MANDATED ask 不提供本档)候 core 5.25.0 出身键 wire 过境,见 #144/[3438]/[3442]。
338
+ * 持久规则候选(#225 件1;0.43.0 起是 {@link RuleOffer} 判别联合)——卡据此渲「不再询问」档;
339
+ * 缺席/空 = 卡形不渲该档。
340
+ *
341
+ * 🔴 **包内单一形出口**(#334/[5223],0.43.0 **BREAKING**):本键**同时**是两代 wire 的落位 ——
342
+ * 新引擎的 {@link ToolApprovalFrame.ruleOffers}(server ≥7.46.0)与旧引擎的
343
+ * {@link ToolApprovalFrame.ruleSuggestions}(≤7.45,归一成 `kind:'single'`)经**同一把**窄读器
344
+ * 合形后落这里。**新键优先、两键同场时旧键整条不看**(绝不混编两代素材)。
345
+ * ⇒ 消费端(cli/web/desktop)读包内这一个形,不感知上游换键。
346
+ * ⚠️ 0.42.0 及更早本键的类型是 sdk `RuleSuggestion[]`(无 `kind`/无 `offerIndex`);
347
+ * 换形是**破坏性**的,呈卡端要按 `kind` 分臂渲(single 一行、batch 一组 + 余量披露)。
348
+ * 🔴 兑付两条路,别混:
349
+ * · `kind:'single'` ⇒ 把该条的 `rule` **原样**放进决断的
350
+ * {@link ApprovalCardAllowDecision.persistRule}(不拼、不改写);
351
+ * · `kind:'batch'` ⇒ 把该条的 `offerIndex` 放进
352
+ * {@link ApprovalCardAllowDecision.persistRuleBatchOfferIndex}(批没有单条文本可抄)。
353
+ * 两位**互斥**,同场 ⇒ 编排层整条持久臂丢弃(见那一位的注)。
354
+ * 🔴 `rule`/`command`/`rules[].segment` 全部 **UNTRUSTED-for-display**。
237
355
  */
238
- ruleSuggestions?: RuleSuggestion[];
356
+ ruleOffers?: RuleOffer[];
239
357
  /**
240
- * 持久规则候选的**只读**对偶([C170] 答问②半场,0.29.0)——原样来自 durable 队列行
241
- * `PendingCheckpoint.ruleSuggestions`(server 7.16.0 [3684]② 补齐的供给)的合形项。
242
- * 🔴 {@link ruleSuggestions} **刻意分键不合流**:那一位的契约是「卡渲可选中的『不再询问』档
243
- * → 决断带 {@link ApprovalCardAllowDecision.persistRule} 回兑」,兑付口=同副本活卡腿
244
- * `respond.persistRule`;而 durable 腿的 `/decide` 体**无规则位**(SDK 6.17.2 PendingCheckpoint
245
- * JSDoc 逐字:display/triage-only)——把行上候选落进那一位,就是一个按下去规则不落地的假
246
- * affordance。本键的不变量是**决断字节**:决断绝不因它带 `persistRule`(或任何规则位)——
358
+ * 持久规则候选的**只读**对偶([C170] 答问②半场,0.29.0;0.43.0 {@link ruleOffers} 换形)——
359
+ * 原样来自 durable 队列行 `PendingCheckpoint.ruleOffers`(server 7.46.0)或旧键
360
+ * `PendingCheckpoint.ruleSuggestions`(server 7.16.0 [3684]② 起的供给,≤7.45)的合形项,
361
+ * 经与活卡腿**同一把**窄读器归一。
362
+ * 🔴 {@link ruleOffers} **刻意分键不合流**:那一位的契约是「卡渲可选中的『不再询问』档
363
+ * 决断带 {@link ApprovalCardAllowDecision.persistRule} / `persistRuleBatchOfferIndex` 回兑」,
364
+ * 兑付口=同副本活卡腿 `respond`;而 durable 腿的 `/decide` 体**无规则位**(SDK 6.17.2
365
+ * PendingCheckpoint JSDoc 逐字:display/triage-only)——把行上候选落进那一位,就是一个按下去
366
+ * 规则不落地的假 affordance。本键的不变量是**决断字节**:决断绝不因它带任何规则位 ——
247
367
  * 「只读」限定的是 wire 回兑通道,不是屏面。渲成可选中项是**允许**的,前提=兑付走客户端
248
368
  * **本地**落规则(cli 1.0.76 起的形:选中 → 客户端写自己的 settings `permissions.allow` 并把
249
369
  * 成败如实上屏,决断仍与两态时代逐字节相同;这正是 CC 第三态的原形——本地文件写)。
370
+ * 🔴 **本腿的 `offerIndex` 不是选择键**:server `boundedRuleOffers` 已逐条丢弃并压紧过一次,
371
+ * 行上的下标本就不等于 core 的 offer index;而这条腿压根没有兑付口 ⇒ 它只是展示/对账座。
250
372
  * 缺席 = 无候选/老行/坏形(三者同形,不猜);既有卡口不读本键 ⇒ 卡形字节不变。
251
373
  */
252
- ruleSuggestionsReadOnly?: RuleSuggestion[];
374
+ ruleOffersReadOnly?: RuleOffer[];
375
+ /**
376
+ * #341/[5214]③(0.43.0;server ≥7.46.0 E-14 / [4537]② Trojan Source 族;**ADDITIVE**,
377
+ * `"tool_approval"` only)——原样来自 {@link ToolApprovalFrame.inputHasBidi}:这只 ask 的
378
+ * **工具输入**里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符)。
379
+ *
380
+ * 🔴 **缺席绝不折成 `false`**(类型 `true`,与 {@link governanceForced}/{@link requiresRealApproval}
381
+ * 同族):缺席同时覆盖「真的没扫到」与「args 序列化不了所以扫不了」两形,读成「已确认干净」
382
+ * 就是对用户下一个证不出的断言。
383
+ * 🔴 **披露位,不是清洗位;本包字节零改**:清洗会改掉即将被执行的那串字节(卡上显示的与真跑的
384
+ * 不是同一个东西),比不披露更坏。显形(转义/高亮/加标记)归端 —— 壳侧基线是
385
+ * `renderUntrustedCommandText`(core 导出的展示基线)。本包只保证这一位到得了卡口。
386
+ */
387
+ inputHasBidi?: true;
253
388
  /**
254
389
  * 被越级的持久 allow 规则**原文**(#144,原样来自 {@link ToolApprovalFrame.persistedRuleShadowed}
255
390
  * 的合形值)——壳据此渲「你的规则仍在,只是这次调用被要求逐次确认」;缺席 = 卡形与 0.27.0
@@ -431,13 +566,39 @@ export interface ToolApprovalFrame {
431
566
  */
432
567
  delegation?: ToolApprovalDelegation;
433
568
  /**
434
- * server 7.12.0(#154 车二 / design/179,ADDITIVE)——**引擎铸的**持久规则候选。只在
435
- * `capabilities.permissionRules` 谓词为真且引擎对本次裁决真铸出候选时在场;缺席 = 这张卡
436
- * 不提供「不再询问」档(老 server / 规则店未接 / 引擎判无候选,三者同形,不猜)。
437
- * 🔴 文本由 ENGINE 铸,不由客户端拼:回决时只能把其中一条**原样**报回
438
- * (`respond` `persistRule.rule`),报表外文本 = server 拒 `rule_not_offered`。
569
+ * ⚠️ **退役键**(server 7.45;7.46.0 起同文件 0 命中 —— engine fixture 直证)
570
+ * server ≥7.12.0(#154 车二 / design/179)起的**引擎铸的**持久规则候选,7.46.0 被
571
+ * {@link ruleOffers} 整体取代(#334/[5223],core 5.58.0 design/375 的判别联合换形)。
572
+ *
573
+ * 🔴 **保读不保写**:本包对它只做**归一读**(`kind:'single'` {@link RuleOffer}),理由 =
574
+ * 7.44 及更旧的引擎今天仍在场(pin 未抬的部署、混舰队)。新键在场时**本键整条不看**。
575
+ * 包内出口只有 {@link ApprovalCardRequest.ruleOffers} 一个,消费端不感知这次换键。
576
+ * 🔴 本键留在 interface 与 {@link TOOL_APPROVAL_FRAME_KEYS_MIRROR} 里**不是**遗留包袱:
577
+ * SDK 7.2.0 的运行期锚 `TOOL_APPROVAL_FRAME_KEYS` 仍含它,删掉会让对账门的
578
+ * 「SDK 锚的每个键本仓都有」当场红 —— 退役登记要等 SDK 跟着 server 一起退。
439
579
  */
440
580
  ruleSuggestions?: RuleSuggestion[];
581
+ /**
582
+ * server ≥7.46.0(#334/[5223];core 5.58.0 design/375;**BREAKING 换形**,`"tool_approval"` only。
583
+ * 来源锚 = engine 7.46.0 fixture `@sema-agent/server/dist/tool-approval.d.ts` 的
584
+ * `ruleOffers?: readonly RuleOffer[]` 声明 + `@sema-agent/core/dist/core/permission-rule-model.d.ts`
585
+ * 的 `RuleOffer` 逐字契约)——引擎为这次 ask 铸的「不再询问」选项,判别联合 `single | batch`。
586
+ *
587
+ * 🔴 **序即契约**:whole-string exact 的 `single` 在场时恒 index 0、`batch` 至多一条且恒末位;
588
+ * core 契约基数 ≤2,server 侧执法帽 `MAX_RULE_OFFERS`=4(容忍余量,消费端按 ≤4 布局)。
589
+ * **选择键是原始下标** —— 本包的窄读器逐条丢坏形并把原始下标记在 {@link RuleOffer.offerIndex}
590
+ * 上(压紧会让「人点的第 k 个」与「服务端兑的第 k 个」指向两条不同规则)。
591
+ * 🔴 **在场性即承诺**:只在**规则店真装配**时在场;缺席 = 这张卡不提供「不再询问」档
592
+ * (老 server / 规则店未接 / 引擎判无候选,三者同形,不猜)。
593
+ * 🔴 文本由 ENGINE 铸,不由客户端拼:single 臂回决只能把 `rule` **原样**报回
594
+ * (`respond` 体 `persistRule.rule`,报表外文本 = server 拒 `rule_not_offered`);
595
+ * batch 臂没有单条文本可抄,回决报**下标**(`respond` 体 `persistRule.batchOfferIndex`)。
596
+ * 🔴 本键**领先** SDK 运行期锚一代(sdk 7.2.0 的 `TOOL_APPROVAL_FRAME_KEYS` 只有退役键
597
+ * `ruleSuggestions`)⇒ 对账门(run-approval-frame-keys-test.mjs)AHEAD_OF_ANCHOR 带退出条件登记。
598
+ * 耐久路对偶 = `PendingCheckpoint.ruleOffers`(同契约、**下标语义不同**:那条腿 server 已逐条
599
+ * 丢弃压紧过,且无兑付口 ⇒ 下标只是展示座),由 {@link surfaceFsApprovalAndDecide} 消费。
600
+ */
601
+ ruleOffers?: readonly WireRuleOffer[];
441
602
  /**
442
603
  * server ≥7.13.0(#144 / core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,
443
604
  * `"tool_approval"` only。来源锚 = engine fixture `@sema-agent/server/dist/tool-approval.d.ts`
@@ -538,6 +699,40 @@ export interface ToolApprovalFrame {
538
699
  * run-durable-card-display-keys-test.mjs ⑨ 段;上游补位后按 {@link probeCause} 的双源合流形跟批。
539
700
  */
540
701
  requiresRealApproval?: true;
702
+ /**
703
+ * #341/[5214]③(server ≥7.46.0;E-14 / [4537]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only。
704
+ * 来源锚 = engine 7.46.0 fixture `@sema-agent/server/dist/tool-approval.d.ts` 的 `inputHasBidi?: true`
705
+ * 声明 + 同包 `dist/tool-approval.js` 的条件 stamp;7.44 fixture 同文件 0 命中)——这只 ask 的
706
+ * **工具输入**(`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符)。
707
+ *
708
+ * 🔴 **真才带,缺席绝不编 `false`**(与 {@link governanceForced}/{@link requiresRealApproval} 同形)。
709
+ * 缺席同时覆盖「真的没有」与「args 序列化不了(循环引用/抛错的 toJSON/BigInt)所以扫不了」两形,
710
+ * 消费端**禁**把缺席读成「已确认干净」。
711
+ * 🔴 **披露位,不是清洗位;字节零改**:审批卡是唯一一处把模型写的命令交给**人眼**判断的面,而
712
+ * bidi 覆写让人眼读到的顺序与真正执行的字节顺序不同 —— 人批准的是 A、跑起来的是 B。清洗会改掉
713
+ * 即将被执行的那串字节,**比不披露更坏**;server 只报「有」,显形归端。本包只做透传
714
+ * (帧 → {@link ApprovalCardRequest.inputHasBidi})。
715
+ * 🔴 判据**不看 `message`**(引擎/策略写的说明文本,不是待执行输入)——两个来源折进一个布尔会让
716
+ * 端无法判断该给哪一段加显形标记。
717
+ * 🔴 本键**领先** SDK 运行期锚一代(sdk 7.2.0 尚无)⇒ 对账门 AHEAD_OF_ANCHOR 带退出条件登记。
718
+ * 耐久路今天**无对偶**(server 7.46.0 的 `PendingCheckpoint`/`riskDescriptor` 均未声明本键;
719
+ * server 侧那份在**卡内**,不是行上的键)⇒ durable 行 → 卡那条腿不 stamp。
720
+ */
721
+ inputHasBidi?: true;
722
+ /**
723
+ * #329 随批小件([4845]-5;**ADDITIVE**,`"tool_approval_complete"` only,真才带;server 7.44.0 及
724
+ * 更早已在场 —— 0.43.0 同形族扫补进镜像的存量漏键,见 {@link TOOL_APPROVAL_FRAME_KEYS_MIRROR} 处注)——
725
+ * `outcome:"expired"` 一词三义(park / 当场 deny / 无设施 deny)里 **park 那一义的显式判别位**:
726
+ * 在场 ⇔ 这条 ask 按 park 路由收尾且墓碑已落(⇔ 同一把 `approvalId` 仍可打 `respond` 走迟到受理)。
727
+ *
728
+ * 🔴 **缺席禁读作「真 deny」**:无店部署 / deny 政策 / 被连坐 VOID 的兄弟都发不出这个键,
729
+ * 缺席只是「无 park 证据」。
730
+ * 🔴 本包今天**只镜像键、不消费**:它只出现在 `tool_approval_complete` 上,而那种帧根本不走卡口
731
+ * (`surfaceToolApprovalFrameAndRespond` 消费的是 `tool_approval`)。要把「卡失效」改渲
732
+ * 「已转后台候批」是一次**行为面**改动(消费者=端的完成帧处理面),按宪法三问单独立项。
733
+ * 登记在此 = 写明理由的未消费,不是没人发现的丢键。
734
+ */
735
+ parked?: true;
541
736
  /**
542
737
  * server ≥7.34.0(#288/[4429]② —— cli 自己请托的窗三键,**ADDITIVE**,`"tool_approval"` only;
543
738
  * 来源锚 = sema-server src/tool-approval.ts 的三键声明+emit 前赋值,字段注逐字「新壳借此给旧族帧
@@ -573,7 +768,7 @@ export interface ToolApprovalDelegation {
573
768
  * `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
574
769
  * 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
575
770
  */
576
- export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
771
+ export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
577
772
  /**
578
773
  * 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
579
774
  * (server 1.258 [1549]①3,旧代际兼容)。
@@ -633,6 +828,31 @@ export interface RespondToolApprovalOpts {
633
828
  * (顶层新键在老 server 上是静默 200 什么都不落,坏于诚实降级)。
634
829
  */
635
830
  persistRuleEdited?: true;
831
+ /**
832
+ * #334 批臂(0.43.0;server ≥7.46.0,design/377):人勾的是一条**合取批** offer,选择键 =
833
+ * 该 offer 在帧上的**原始 wire 下标**(编排层已做「该下标真是 batch offer」的表核与三臂互斥)。
834
+ *
835
+ * 🔴 **注入面的映射义务**(本包只到这一格,wire 体由宿主/SDK 铸):server 的 respond 体形是
836
+ * `persistRule: { batchOfferIndex }`,与文本臂 `persistRule: { rule, edited? }` **同一个字段名、
837
+ * 三臂互斥**。本包刻意保持**扁平兄弟位**(既有 `persistRule: string` 字节不变),由注入面合成:
838
+ * ```
839
+ * persistRuleBatchOfferIndex !== undefined
840
+ * ? { batchOfferIndex: persistRuleBatchOfferIndex }
841
+ * : persistRule !== undefined
842
+ * ? { rule: persistRule, ...(persistRuleEdited === true ? { edited: true } : {}) }
843
+ * : undefined
844
+ * ```
845
+ * 🔴 **三位绝不同时上体**:server 对同场组合响亮 400 且**连决断一起拒**。编排层已保证本包发出的
846
+ * opts 上文本臂与批臂不会同场,注入面只需按上面的优先序取一个,**不要**自己再补一个 fallback。
847
+ * 🔴 **宿主必须供给能力位**(异源对抗复审五轮 [medium] 订正 —— 本段初稿写的是「批臂供给自闸、
848
+ * 老引擎零受迫」,与实装**相反**,而这段注释随 `.d.ts` 出包 = 三端读到的接入契约,与
849
+ * {@link ApprovalCardAllowDecision.persistRuleBatchOfferIndex} 那段同名残余同批清):
850
+ * 编排层只在 {@link ToolApprovalFrameLaneOpts.respondBatchRuleOffersCapable} `=== true` 时填本位。
851
+ * **宿主漏供这一位 ⇒ 现代引擎上每一次批臂勾选都被丢**(决断照送 + 一条 `surfaceRuleArmNotSent`
852
+ * 通知)。「老引擎不铸 batch offer 所以本位恒缺席」这句**单独看仍然成立**,但它证不出安全性:
853
+ * 滚动升级 / 多副本 / 代理下,**出帧的那台**与**收 respond 的那台**可以不是同一个版本。
854
+ */
855
+ persistRuleBatchOfferIndex?: number;
636
856
  /** #229(server ≥7.15.0):回决备注 —— 与 durable 腿 `AskDecisionBody.note` **同词同源同一列**
637
857
  * (`decision_note`,≤2048)。任何 decision 都可带(deny 的「为什么拒」正是审计面上最值钱的
638
858
  * 一条);真落行与否看 ack 的 {@link surfaceToolApprovalFrameAndRespond} 消费的 `noteRecorded`。
@@ -640,6 +860,48 @@ export interface RespondToolApprovalOpts {
640
860
  note?: string;
641
861
  }
642
862
  export type RespondToolApprovalFn = (approvalId: string, decision: ToolApprovalRespondDecision, opts?: RespondToolApprovalOpts) => Promise<ToolApprovalRespondAck | void>;
863
+ /**
864
+ * respond 回执的**包内视图** —— sdk `ToolApprovalRespondAck` 的 additive 超集(#334,0.43.0)。
865
+ *
866
+ * 为什么是超集而不是直接用 sdk 那个形:server 7.46.0 的兑付回执上有两个**规范文本回显位**,而
867
+ * sdk 7.2.0 的 `ToolApprovalRespondAck` 声明里还没有它们(与 `ruleOffers` 同一次锚滞后)。缺这两位,
868
+ * 「规则真正存成了什么样」在包边界上被吞掉 —— 而编辑臂的落盘文本**可能与人敲的原字节不同**
869
+ * (core 会把 `Bash(adb *)` 规范成 `Bash(adb:*)`),批臂更是一次落多条。
870
+ *
871
+ * 🔴 **超集只加不改**:继承 sdk 那个形的每一位,语义与字节零改动;既有消费端读 `rulePersisted` /
872
+ * `ruleRefusal` / `noteRecorded` 的写法一字不用动(返回型变宽对读方是源码兼容的)。
873
+ * 🔴 sdk 锚补上这两位那天,本形按 `probeCause` 先例回收(直接用 sdk 形)。
874
+ */
875
+ export interface ToolApprovalRespondAckView extends ToolApprovalRespondAck {
876
+ /**
877
+ * server ≥7.44(#340 编辑臂回显 `editedArmEcho`;7.46 沿用)——**编辑臂**兑付成功时,规则店里
878
+ * **真正落盘**的那条规范文本。
879
+ * 🔴 **界面要回显的是这一份,不是输入框里那一份**:core 会规范化人敲的拼写。
880
+ * 只在编辑臂 ∧ 兑付成功 ∧ 落的是 single 时在场;候选臂/批臂/失败/老 server ⇒ 缺席(≠ 空串)。
881
+ */
882
+ persistedRule?: string;
883
+ /**
884
+ * server ≥7.46.0(design/377 批臂回执)——**批臂**兑付成功时落地的**全体**成员规范文本,
885
+ * **展示序**(= batch offer 的成员序,壳逐条回显)。「落地」含 persisted 与 deduped 两种
886
+ * (等价规则已在店 = 同意已生效)。
887
+ * 🔴 **复数是契约不是巧合**:合取批一次授权多条规则,把它折成一条(或只报第一条)会让人以为
888
+ * 自己只批了一条。非数组 / 含非串成员 / 空数组 ⇒ 整只降缺席(半份清单比没有清单更坏)。
889
+ * 非批臂 / 兑付失败 / 老 server ⇒ 字段省略;**缺席 ≠ 空**。
890
+ *
891
+ * 🔴 **在册残余:逐条归属今天 wire 上证不出**(异源对抗复审三轮 [medium] 采纳为**登记**,不是修复)。
892
+ * 本包对这一位做到的相关性是 **id + decision + 基数**({@link surfaceToolApprovalFrameAndRespond}
893
+ * 的臂相关性门):条数必须等于所选 batch 的成员数。基数抓得到「少报/多报」,**抓不到**
894
+ * 「条数对但内容是另一批规则」—— 因为兑付回执上**没有任何锚**能把这 N 条与那只 offer 对上
895
+ * (回执不回 `batchOfferIndex`,成员也没有稳定 id;而文本等式在这一位上不成立:落盘的是
896
+ * **规范化后**的文本,与 offer 上的文本按设计可以不同,拿文本去比就是装第二个判官)。
897
+ * 🔴 ⇒ **端的呈现口径**:把它渲成「**引擎报告**落盘的规则」,**不是**「你刚才选的那批规则」。
898
+ * 两句话在正常情形下指同一件事,但只有前一句是本包能背书的。
899
+ * 正位解需要**上游补一个 additive 锚**(回执带 `batchOfferIndex`,或成员带稳定标识/摘要)——
900
+ * 已登记 `docs/INTEGRATION-CLIENTS.md` §7 缺口 **P-39**,按接入文档宪法向 server 点名索要,
901
+ * 补上当天本注按 `probeCause` 先例跟批收紧。
902
+ */
903
+ persistedRules?: readonly string[];
904
+ }
643
905
  /**
644
906
  * respond 回执的**结构化读口**(wire 是 JSON:注入面可能是旧 liveClient 的 raw fetch,也可能是
645
907
  * 比本包新一版的 SDK)。坏形一律降 `undefined` —— 与 `controlRouter.errCodes` 同族纪律:
@@ -651,7 +913,7 @@ export type RespondToolApprovalFn = (approvalId: string, decision: ToolApprovalR
651
913
  * 当成合规 ack 放行。**相关性**(「这个 ack 是不是**这一次**审批的回执」)不在本函数判 —— 本函数
652
914
  * 只认形,对不对得上由 {@link surfaceToolApprovalFrameAndRespond} 拿着 frame 与刚发出的 decision 核。
653
915
  */
654
- export declare function readToolApprovalRespondAck(v: unknown): ToolApprovalRespondAck | undefined;
916
+ export declare function readToolApprovalRespondAck(v: unknown): ToolApprovalRespondAckView | undefined;
655
917
  /**
656
918
  * 一张 `tool_approval` 帧消费完的产物(FIX①,2026-08-07 **BREAKING**:此前是裸的
657
919
  * `ToolApprovalRespondDecision | 'unresolved'` 字符串)。
@@ -660,8 +922,9 @@ export declare function readToolApprovalRespondAck(v: unknown): ToolApprovalResp
660
922
  export interface ToolApprovalFrameOutcome {
661
923
  /** 用户(或 fail-closed)落定的决断;`'unresolved'` = respond 没送达(引擎按 TTL 自决)。 */
662
924
  decision: ToolApprovalRespondDecision | 'unresolved';
663
- /** server 的 200 ack。**缺席 = 未知**(注入面回 void / server / respond 失败),绝不当成 false。 */
664
- ack?: ToolApprovalRespondAck;
925
+ /** server 的 200 ack(0.43.0 起是 {@link ToolApprovalRespondAckView} —— sdk 形的 additive 超集,
926
+ * 多两个规范文本回显位)。**缺席 = 未知**(注入面回 void / 旧 server / respond 失败),绝不当成 false。 */
927
+ ack?: ToolApprovalRespondAckView;
665
928
  /**
666
929
  * #225 件5(0.42.0):respond **抛错**那一支的原文交还位。
667
930
  *
@@ -753,12 +1016,44 @@ export interface ToolApprovalFrameLaneOpts {
753
1016
  * server 能力位 `capabilities.respondFreeFormRules` 的读数(宿主从自己的 caps 缓存供给;
754
1017
  * server ≥7.44,#340 [5071] 恒真版本位)。
755
1018
  * 🔴 形与判据**逐字照** {@link approvalDecisionNoteCapable} 既有形(同一条 SDK 6.16 成文纪律:
756
- * **位缺席就别发**)—— 老 server 对未知请求键静默忽略且照回 200,「这台不认识自由文本臂」与
757
- * 「记上了」在响应上不可分,发了只造「规则已存」的错觉。
1019
+ * **位缺席就别发**)
758
1020
  * 缺席/false ⇒ 编辑臂的 `persistRule` **整条不发**(fail-closed 到「不发」侧;决断本身照常送达,
759
- * 现状字节不变,老引擎零受迫)。候选臂**不受本位影响**(它是 0.25.0 起就有的既有通道)。
1021
+ * 现状字节不变)。候选臂**不受本位影响**(它是 0.25.0 起就有的既有通道)。
1022
+ *
1023
+ * 🔴 **失效形订正**(异源对抗复审七轮 [medium];本段初稿把 `note` 那一位的失效形抄了过来):
1024
+ * 编辑臂**不是**「老 server 静默忽略且照回 200」——`[5071]` 选 `persistRule.edited` 这个键名
1025
+ * 的**全部理由**就是让老 server **诚实降级** `rule_not_offered`,而形/上限不合时更是响亮 400
1026
+ * (`PERSIST_RULE_TEXT_ERROR` / `PERSIST_RULE_EDITED_FLAG_ERROR`),**400 连决断一起拒**。
1027
+ * ⇒ 本位存在的理由不是「分辨静默 200」,是「能提前知道这台不供,就别把人的编辑送出去白跑一趟、
1028
+ * 更别让它把决断打掉」。
1029
+ * 🔴 **「老引擎零受迫」这句话有前提**(§7 **P-40**):本位是**per-baseUrl 的布尔缓存**,
1030
+ * **不与收 respond 的那个副本绑定**。引擎温切 / 滚动升级 / 多副本代理下,一个陈旧的 `true`
1031
+ * 仍会把新形送到老副本上。宿主的义务:引擎 respawn/restart 后调
1032
+ * `invalidateEngineCaps(baseUrl, probe?)`;滚动窗口内宁可按 `false` 供(丢持久臂 + 一条
1033
+ * `surfaceRuleArmNotSent` 通知,远好于把人按下的决断打掉)。
760
1034
  */
761
1035
  respondFreeFormRulesCapable?: boolean;
1036
+ /**
1037
+ * server 能力位 `capabilities.respondBatchRuleOffers` 的读数(宿主从自己的 caps 缓存供给;
1038
+ * server ≥7.46.0,#334 design/377 恒真版本位 —— 直证 = engine 7.46.0 fixture 的
1039
+ * `@sema-agent/server/dist/http/routes/capabilities.js` 里 `respondBatchRuleOffers: true`)。
1040
+ *
1041
+ * 🔴 形与判据**逐字照** {@link respondFreeFormRulesCapable}(同一条 SDK 6.16 成文纪律:
1042
+ * **位缺席就别发**)。缺席/false ⇒ 批臂的 `persistRuleBatchOfferIndex` **整条不发**
1043
+ * (fail-closed 到「不发」侧;决断本身照常送达,现状字节不变)。
1044
+ * 🔴 **为什么「帧上有 batch offer」不能顶替这道门**(异源对抗复审 [high] 采纳,施工时实证
1045
+ * ——该 caps 位在 7.46.0 上确实存在):供给面只证明**出帧的那一端**认识新形,不证明
1046
+ * **收 respond 的那个端点**认识它。滚动升级 / 多副本 / 代理后面站着一台 ≤7.45 的实例时,
1047
+ * 旧解析器对 `persistRule.batchOfferIndex` 走的是「`persistRule.rule` 必须是非空串」那条,
1048
+ * **响亮 400 且连决断一起拒** —— 于是人按下的那次 allow 落不了地,退化成 unresolved/TTL 自决。
1049
+ * 与 note 超限那条同一条纪律:附带愿望的失败绝不许升级成主动作的失败。
1050
+ * 🔴 **本位是 per-baseUrl 缓存,不与 responder 绑定**(§7 **P-40**,与
1051
+ * {@link respondFreeFormRulesCapable} 同族):陈旧的 `true` 仍会把新形送到老副本上 ⇒
1052
+ * 引擎温切后必须 `invalidateEngineCaps(baseUrl, probe?)`,滚动窗口内宁可按 `false` 供。
1053
+ * 🔴 **只闸兑付,不闸展示**:位缺席时 batch offer 照旧上卡(`ApprovalCardRequest.ruleOffers`
1054
+ * 字节不变)—— 端可以渲、可以走本地落规则那条路,只是**这条 wire 兑付通道**不开。
1055
+ */
1056
+ respondBatchRuleOffersCapable?: boolean;
762
1057
  /**
763
1058
  * 这条帧是不是**当下**从 live 流上收到的(异源对抗复审 P1 采纳;壳孪生帧族同名闸的
764
1059
  * 本包席位 —— 壳 `approvalStreamWire` 头注逐字:「账本重放的历史帧带的是铸帧时刻的旧余量,