@sema-agent/client-core 0.56.0 → 0.58.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,299 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.58.0(2026-09-06)
53
+
54
+ > 内容批(2026-09-06,隔离树交付):三件 —— ①`@sema-agent/sdk` **8.2.0 提货**(peer 地板抬版,
55
+ > 本版**唯一**的非 additive 面)、②**B-025 修根**(「不再询问」候选窄读器不再手抄上游闭词表)、
56
+ > ③**L-102 下半场**(resume 拒绝**文案铸点**上收,壳侧改薄成适配层)。**本段只记内容**:
57
+ > `package.json.version` 未动、README Version 行未动、`FROZEN` 未动 —— bump 与段头转日期形归
58
+ > **发包批**(阶段一义务见本档头注)。
59
+ > 接入面详报见 `docs/INTEGRATION-CLIENTS.md` **§22**(三件各一小节 + 端的迁移动作 + 门表)。
60
+
61
+ **🔴 唯一的非 additive 面 = peer 地板抬到 `@sema-agent/sdk >=8.2.0`**(上一次是 0.48.0 的 `>=7.4.0`)。
62
+ 四条硬理由(逐条也写在 `scripts/run-sdk-floor-test.mjs` 的 `FLOOR` 注里,不是顺手跟版本):
63
+
64
+ 1. **三张 `as const` 闭词表是运行期判据** ⇒ 值级 import:`RULE_OFFER_MATCHES` /
65
+ `RULE_OFFER_BATCH_MEMBER_KINDS` / `RULE_OFFER_UNCOVERED_REASONS`(sdk 8.2.0 S-134 锚②)。
66
+ `hitl/toolApprovalWire.ts` 的窄读器直接读它们,<8.2.0 上这三个名字不存在,**整包编译不过**。
67
+ 这一条单独就足以钉死地板。
68
+ 2. `RuleOffer` / `RuleOfferMatch` / `RuleOfferBatchMember` / `RuleOfferUncoveredDetail` /
69
+ `RuleOfferUncoveredReason` **五个类型面**(同锚)—— 本包归一形现由它们派生。
70
+ 3. `ToolApprovalRespondAck` 上的 `persistedRule` / `persistedRules` / `persistedRuleAnchors` 三位 +
71
+ 导出的 `PersistedRuleAnchor`(S-134 锚③)—— 0.43.0 起的包内 additive 超集
72
+ `ToolApprovalRespondAckView` 按它**自己写下的退役条款**回收成 sdk 形的别名。
73
+ 4. `AgentEvent` 新增 `approval_revoke` 臂 —— 臂一进 union,`eventToSdkMessage` 的 B5 编译期穷举断言
74
+ `assertNeverArm` **当场红**(本批实翻:装上 8.2.0 的当拍 `tsc` 就是这一条错)。
75
+
76
+ 🔴 **同批亲验(别只信版本号)**:实装 8.2.0 的 `dist/resources/tool-approvals.js` 直读 ——
77
+ `RULE_OFFER_MATCHES` = `["exact","prefix","wildcard","subpath"]`(**四员**;⚠️ 不是 B-025 票面写的
78
+ "wildcard/glob")、`RULE_OFFER_BATCH_MEMBER_KINDS` = `["command","directoryRead"]`、
79
+ `RULE_OFFER_UNCOVERED_REASONS` = `["redirection","no_rule_form","cap_overflow"]`、
80
+ `TOOL_APPROVAL_FRAME_KEYS` **18 项且含 `ruleOffers`**。
81
+
82
+ ---
83
+
84
+ ### 件① sdk 8.2.0 提货(锚追平 / 两笔到期记账退役 / 一条新臂进 switch)
85
+
86
+ - **`AHEAD_OF_ANCHOR` 的 `ruleOffers` 领先登记退役**(`run-approval-frame-keys-test.mjs`)——
87
+ 这是**登记自己的退出条件到期**,不是清理:sdk 8.2.0 的运行期锚已含该键 ⇒ 门先**自红一次**
88
+ (红词逐字「领先登记 ruleOffers:SDK 锚**仍无**此键 —— 锚补上了就该删这条登记」)再删,与
89
+ `persistedRuleShadowed`(#229)/ `probeCause`(#280 件1)两次走同一条路。
90
+ ⚠️ **同批不顺手退别的**:`requiresRealApproval` / 窗三键 / `inputHasBidi` / `parked` 在 8.2.0 的锚上
91
+ **仍然没有**(同一次 node 直读)—— 退出条件是**逐条**的,不是整表的,表回空才是健康态。
92
+ - **`ToolApprovalRespondAckView` 与 `PersistedRuleAnchor` 回收成 sdk 形的别名**(名字保留 ⇒ 端源码
93
+ 兼容,读法逐字不变)。判形与窄读**一个字节没改**:`readToolApprovalRespondAck` 的每一条纪律照旧
94
+ (三位规范文本回显整只判形 / 单复数同场即矛盾 / 回显在场而 `rulePersisted !== true` 即矛盾 /
95
+ 锚与清单逐位置同文本)—— 上游补的是**类型**,不是判官。
96
+ - **`approval_revoke` 臂进 switch**,处置 = `dropped/unsupported_arm`(**有痕**,经
97
+ `reportDroppedFrame` 走宿主 sink),与兄弟 `approval_request` 同姿势、同理由:本包今天对新族两帧
98
+ **零消费口**(`isToolApprovalFrame` 只认 legacy 两词),而撤卡帧带的是用户可见的状态迁移,
99
+ 悄悄丢掉比吼一行更坏。缺口如实记账,接那条新卡链是另一批。
100
+ - **§7d P-44 转「已收」**:sdk 8.1.0 那一族型面缺口在 8.2.0 全部补齐。🔴 本包侧**本来就零 cast**
101
+ (P-44 原文已写清:本包不铸 respond 的 wire 体,吃缺口的是三端**注入面**)⇒ 剩余动作在三端 ——
102
+ 注入面那处宽 `cast` 可以删掉,改用 sdk 8.2.0 的 `PersistRuleSelection`(两臂互斥 `?: never` 钉在编译期)。
103
+
104
+ ### 件② B-025 修根:窄读器不再手抄上游闭词表(病形 = 手抄的判据跟不上单源)
105
+
106
+ **病形**(0.43.0 铸、0.57.0 仍在;由 sdk [6464] 两侧源码对读点名,坐标
107
+ `src/hitl/toolApprovalWire.ts:1772` / `:209`,cli [6465] 认账立 B-025):
108
+
109
+ | 症状 | 0.57.0 实际行为 | 用户看到什么 |
110
+ |---|---|---|
111
+ | ① `match` 词表**手抄**成两员 `'exact' \| 'prefix'`,而 core design/382 / #510 已扩到四员 | 一条合法的 `match:"wildcard"`(或 `"subpath"`)single **整条丢** | 卡上那一格「不再询问」**凭空消失** |
112
+ | ② batch 成员**手抄**成「三元组 + segment」,没有 `directoryRead` 臂 | 一只带 `cd` 段目录只读授权的 batch **整只丢** | 同上;且与该函数自己注释承诺的「逐条丢坏、不整拒」相反 |
113
+ | ③ 没有 `uncoveredDetail` 座 | design/382 §3.5 的 additive 明细整段读不到 | 「为什么这段还会问」无从渲 |
114
+
115
+ **修**(不是补两个词,是**取消抄本**):三张闭词表单源化到 sdk 8.2.0,本包再导出同一份数组对象
116
+ (端拿它判定,不再手抄字面量);`RuleOfferMatch` / `RuleOfferBatchMember` /
117
+ `RuleOfferUncoveredDetail` / `RuleOfferUncoveredReason` 四形改由 sdk 派生。
118
+
119
+ - 🔴 **`match` 词集双向全等钉**(写进门):⊇ 表里**每个**词的 single 都必须被读器认下(手抄的窄表
120
+ 在新词上当场红,那正是症状①);⊆ 构造出来**保证在表外**的词必须被丢(读器放宽成「是串就收」
121
+ 时当场红)。退役键 `ruleSuggestions` 那条归一臂共用同一把三位窄化 ⇒ 新词表在那条腿上同样生效
122
+ (族扫:病形不许只修当格)。
123
+ - 🔴 **成员 `kind` 不识 ⇒ 丢整只 batch,绝不丢单个成员,也绝不丢整卡** —— 这是 core design/382 §2.3
124
+ 的**规范性降级臂**(逐字 "drops the WHOLE batch offer — never the single member ... and never the
125
+ whole card";sdk 8.2.0 顶注同文,上游发布帖 ④2 的黑盒判据亦同)。⚠️ **B-025 票面「成员级容错」
126
+ 那句与三处上游原文相反,本批按上游原文实现并把它钉成正向断言**(直接影响:`directoryRead` 因为
127
+ **被认回**而整只留下,而不是因为「容错」被拆散)——理由见 §20c 第 3 条。
128
+ - ⚠️ **另收一条 pre-B3 兼容臂**:`kind` **缺席**的成员按 `kind:'command'` 归一(server ≥7.46.0 到
129
+ design/382 §2.3 B3 落地之间铸的成员没有判别位;窄读域只许等于或宽于铸点域)。归一形上 `kind`
130
+ **恒在场**,端拿到的成员永远是判别联合,零分支差异。
131
+ - **additive `uncoveredDetail` 透传**:🔴 **缺席 ≠「没有未覆盖段」**(server 对坏形的座只丢座不丢批,
132
+ 老引擎压根不铸;真源恒是 `uncoveredSegments` 那个 count)⇒ 缺席时**不铸空数组**;坏行**只丢那一行**,
133
+ 坏载体只让**本位**降缺席,**绝不**因为一个 additive 位否决整只 batch;行数与 count **不强制相等**。
134
+
135
+ **异源对抗复审 r1 两条 [medium] 全采(真病,均在本批修掉)**:
136
+ - 🔴 **`uncoveredDetail` 行数帽 8 → 32(比铸点域更窄的窄读域,[4050] 禁的形)**:初稿按「与成员帽
137
+ 同源同量级」类比出一个 8,而那是**类比不是取证**、且判错了源 —— server 的执法帽是
138
+ `MAX_UNCOVERED_DETAIL_ROWS = 32`,且**未覆盖段数根本不受成员帽约束**(core 逐字
139
+ `uncoveredSegments: uncoveredDetail.length`,一只**只有 1 个成员**的 batch 可以带 9 条明细:
140
+ 9 个带重定向的段各自 mint 不出规则形,进不了 `rules[]` 却每段都要给一个「为什么还会问」)。
141
+ 后果 = 一只合法的 9~32 行明细座被整座静默丢掉,人失去「这几段为什么还会问」。取**等于**铸点域
142
+ 而不是再自行加倍(与 `MAX_RULE_OFFERS_TOLERATED` 不同:那一位没有可锚的上游常量,余量是纯容忍;
143
+ 本位有具名铸点常量,再加倍就是又一次凭感觉编数)。边界钉进门 ⑨d:9 / 32 必收、33 才丢。
144
+ - 🔴 **成员 `kind` 的判据本身还是手抄的**(同形残余,异源对抗复审的负控质疑逼出来的):初稿把
145
+ `'command'` / `'directoryRead'` 两个词**手写在分支条件里**,导入的 `RULE_OFFER_BATCH_MEMBER_KINDS`
146
+ 只被再导出、运行期一次都没读 —— 那正是 B-025 本身的病形(`match` 手抄两员)在成员这一维上的
147
+ 残余,只是当天恰好抄对了。修 = 成员性判到闭词表 + 臂分派兜底(**表内但本包还没有读器**的 kind
148
+ 与表外同处置:丢 ⇒ 整只 batch 丢,fail toward asking;而门 ⑨c 的 ⊇ 腿会当天红逼人补读器)。
149
+ 同批把该条负控从「缺字段的坏成员」换成「**完全合形、只有 kind 在表外**」——否则 `readRuleTriple`
150
+ 会替 kind 判据把它拒掉,负控量不到该量的东西。
151
+ ⚠️ 诚实记录:修完之后产品侧有**两道**判据一起挡它,**只删表检查**是一次**行为不变**的变异(门照绿
152
+ 是正确的);把两道**一起**削回初稿结构才是真回归形状,门当场红(收货亲跑实证:该变异下读器返回
153
+ `["single@0","batch@1"]`,坏批被放行)。
154
+
155
+ **⚠️ 端要跟的型面差分**(本件唯一会让端编译红的地方,而红是要的):`RuleOfferMatch` 从两员变四员 ⇒
156
+ 对它写穷尽 `switch` 的端会在 `wildcard` / `subpath` 两格红。那两格此前在端上是**静默不可达**的
157
+ (包在读器里就丢掉了),不是「新增的空格子」。`RuleOfferBatchMember` 变判别联合 ⇒ 渲成员前先读 `kind`。
158
+
159
+ **同形存量族扫**(病形 = 「本包手抄上游 `as const` 闭词表」):sdk 8.2.0 一共导出 5 张这样的表,
160
+ 另两张(`CONTROL_ROLES` / `NOT_FOUND_TASK_OWNER_CODES`)在本包 `src/**` **零命中**(亲跑 grep,
161
+ 含各自的成员字面量)⇒ 本族**只有规则报价这三张**,已全部单源化,无残余。
162
+
163
+ ### 件③ L-102 下半场:resume 拒绝**文案铸点**上收(新公面 `src/resumeRefusalCopy.ts`)
164
+
165
+ 0.57.0 上收的是**事实读数**(§21 `resumeRetryLaterFromError`);留在壳里的另一半是**人话** ——
166
+ cli 1.0.x `src/sema/resumeRefusalCopy.ts` 自铸了一份判型 + 三句 `·` 分段文案,而 web-client /
167
+ desktop 要么没有、要么将来会再抄一份。三端各抄一份 = 同一次拒绝在三个端上说三句不一样的话,
168
+ 而这三句回答的是**同一个安全问题**:「这次拒绝有没有消费掉我的决定 / 这张卡还能不能再决」。
169
+
170
+ **新增公面 7 件**(公面导出 **821 → 828**):`RESUME_REFUSAL_CODES` · `resumeRefusalFromError` ·
171
+ `resumeRefusalContent` · `RESUME_PLACEMENT_MISMATCH`(码常量,`engineErrorCodes.ts`)+ 件② 的三张闭词表再导出。
172
+
173
+ - 🔴 **两个闭集刻意分家**:`RESUME_RETRY_LATER_CODES`(§21)闭的是「**server 在哪些码上铸
174
+ `retryAfterSec`**」⇒ 答「能不能等」;`RESUME_REFUSAL_CODES`(本件)闭的是「**哪些 resume 拒绝有
175
+ 人话可补**」⇒ 答「该对人说什么」。两集**交于** `resume.preflight_rejected` 一码、**各有**一个独占
176
+ 成员 —— `usage_window_exhausted` 只在前者(没有专属人话,「你可以等」就是全部信息);
177
+ `placement_mismatch` 只在后者(server 不给它铸窗,而且**等一会儿对它毫无用处**:出路是换 root,
178
+ 是**换参数**不是**等时间**;放进时间性闭集 = 让人白等一个永远不会自己好的拒绝)。
179
+ - 🔴 **不复制判定**:交点码的窗与可等性一律**转调** `resumeRetryLaterFromError`,本模块里没有第二个
180
+ `retryAfterSec` 窄读器(`paired-mechanisms-must-share-premise`:两处各判必然在某一格分叉)。
181
+ 门用九形入参驱动两口,`waitable` 与 `retryAfterSec` 必须**逐字相同**(含「缺席」那一格)。
182
+ - 🔴 **文案第三句由 `waitable` 选,绝不由 message 文本选**(按上游散文分支正是「上游改一个词、端的
183
+ 判定静默空转」的形):`placement_mismatch` ⇒ 说出路(换 root);`preflight_rejected` **无窗** ⇒
184
+ **不可判**,说两条臂都成立的那句、把「还能不能再赎」交给屏上那行引擎原文(cli 1.0.x 现行文案逐字);
185
+ **有窗** ⇒ 那是 retry_later 臂的**充分证据**(core 的 terminal 臂抛的 `CheckpointError` 一个 detail
186
+ 都不带,窗结构上到不了客户端)⇒ 才敢说「障碍清掉之后这个 token 还能再赎」。
187
+ - **闭集是 `Object.freeze` 数组**(与 `RESUME_RETRY_LATER_CODES` 同一条已定谳的病形):公开面与判定源
188
+ 是同一个物,`ReadonlySet` 只在类型面只读,一次 `.add` 就能把「重开吧」挂上安全声明。
189
+ - **文案里不铸秒数**:等待那一行由端单独渲(壳的 `retryAfterHint` 是那一位的唯一取值口),
190
+ 写进文案 = 同一个数字上屏两遍。
191
+
192
+ **⚠️ 壳提货后的行为差分(是修好不是回归)**:`retryAfterSec` 窄读域从 cli 自铸的「有限数 ∧ ≥0」
193
+ 收到与 §21 同律的「**整数 ∧ ≥1**」。server 的铸键逐字是「ms → 秒**向上取整**、**下限 1**」⇒ 真供给里
194
+ 不存在 0 / 负数 / 小数;放行 `0` 就是对端说「立刻重试」,而 resume 是 **AT-MOST-ONCE** 的有副作用动作
195
+ (叫醒 = 真跑一轮)—— 一个 0 会把「等一会儿」变成热循环。
196
+
197
+ **可达性如实登记**(不许把预置说成到货):两码在 server 7.51 当版**结构性不可达**(四条腿都不供
198
+ `placementRoot`,也没有 `resumePreflight` 座)。本模块与壳那份一样是**预置臂**,收进包是为了
199
+ 「码到货那天三端不至于各说各话」,不是在声称这条指路已经上过屏。
200
+
201
+ ---
202
+
203
+ ### 常驻门
204
+
205
+ - 🆕 `scripts/run-resume-refusal-copy-test.mjs`(**103 checks**):公面在场 · 两个闭集关系**双向钉** ·
206
+ 不复制判定(九形入参两口逐字相同)· `placement_mismatch` 恒无窗恒不可等**且带窗也不翻、那个窗也不透传** ·
207
+ 窄读域「整数 ∧ ≥1」· 文案三句由 `waitable` 选而非 message(两族原话灌进 message 而文案一字不动)·
208
+ 负控(同族六码 + 429 同名不同门 + 下划线两族 + 别族 + 裸 404 + 码在 message 里被引用 + 六形坏载体)·
209
+ 结构读不 instanceof · 退役 `code` 槽两码各走一遍都不认 · 纯函数。
210
+ - `scripts/run-rule-offers-reader-test.mjs`:**109 → 169 checks**(新增段 ⑨ 六小节,判据见 §22d)。
211
+ - `scripts/run-approval-frame-keys-test.mjs`:`ruleOffers` 领先登记退役后回到逐元素相等
212
+ (SDK 锚 18 项 / 本仓镜像 24 项 / 领先登记 6 项)。
213
+ - `scripts/run-sdk-floor-test.mjs`:`FLOOR` 8.2.0 与 peer 声明**三位逐位等值**互绑。
214
+ - `scripts/run-public-surface-test.mjs`:公面导出基线 **821 → 828**;peer 地板 + README 三处逐字核。
215
+
216
+ ### 已知局限(本版新增)
217
+
218
+ - **`approval_revoke` / `approval_request` 两帧本包仍零消费口**:处置是有痕的
219
+ `dropped/unsupported_arm`。端不要把「撤卡帧没反应」读成「引擎没发」;接那条新卡链是另一批。
220
+ - **`run-approval-frame-keys-test.mjs` 的 fixture 腿在 server ≥7.57 上红**(本批**非引入方**,
221
+ 实测 7.57/7.58 缺 `ruleOffersAbsence`、7.59 起再缺 `denialLimitFallback`、7.60 起再缺 `origin`)——
222
+ 这是另一条已知病形(「镜像滞后于 server 实发键」)的存量,与本批的病形(「手抄闭词表」)不同族,
223
+ 按坐标上报另立。本批 `npm test` 走的是 SDK 锚腿(fixture 腿在本仓默认不跑,`@sema-agent/server`
224
+ 不是本包的 dep)。
225
+
226
+ ## 0.57.0(2026-09-05)
227
+
228
+ > 内容批 D(2026-09-05,隔离树交付):三件 —— L-103/[C228] 窄读器公面化、L-102 resume 时间性拒绝
229
+ > 判型上收、[C233]① SDK 型面缺口登记。**本段只记内容**:`package.json.version` 未动、README Version
230
+ > 行未动、`FROZEN` 未动 —— bump 与段头转日期形归**发包批**(阶段一义务见本档头注)。
231
+
232
+
233
+ ### 新增 —— 「不再询问」候选窄读器的公面(L-103 / [C228]、[C233]②;additive 导出,语义零变化)
234
+
235
+ 两代 wire 键(`ruleOffers` server ≥7.46.0 / 退役键 `ruleSuggestions` ≤7.45)的归一窄读器此前是
236
+ **模块内私有函数**,包外唯一读得到归一形的路径是卡端口(`ApprovalCardRequest.ruleOffers` /
237
+ `.ruleOffersReadOnly`)。不走卡端口架构的宿主(浏览器端自己拿帧渲,没有 Ink 三选卡)因此只能在
238
+ 自己那边**重铸一遍** —— 而这把读器承载的是**兑付安全**判据,不是格式化:
239
+
240
+ - 逐条丢坏、**原始 wire 下标不前移**(压紧 = 人点的第 k 个与服务端兑的第 k 个指向两条不同规则);
241
+ - `kind` 是闭集判别位(不认识的臂不猜、不降级成 `single`);
242
+ - 两代取舍序(新键在场即定局、有载体读不出**不**回落旧键、`null` ≡ 缺席)。
243
+
244
+ 新公面(两个函数,**签名与函数体一字未改**,只是加了 `export`):
245
+
246
+ - `readRuleOffers(v)` —— 只读新键;
247
+ - `readRuleOfferSupply(offers, legacy)` —— 两代键的取舍口,三端该调的那一个
248
+ (0.56.0 及更早的内部名是 `readOfferSupply`,**纯改名**,零外部消费点)。
249
+
250
+ 归一形 `RuleOffer` / `RuleOfferMatch` / `RuleOfferBatchMember` 与入参形 `WireRuleOffer` 照旧导出 ——
251
+ 端拿得到值也拿得到形,不必再自铸一份会漂的同名结构。
252
+
253
+ 🔴 **`offerIndex` 的定义域随腿不同**,消费前必读 §20c:活卡帧腿上它是合法**选择键**
254
+ (批臂按它回兑 `persistRule.batchOfferIndex`),durable 行腿上它只是展示/对账座(该腿无兑付口)。
255
+
256
+ ### 新增 —— resume 族「时间性拒绝」二码的判型(L-102;server ≥7.47.0 / ≥7.51.0,SDK 8.1.0)
257
+
258
+ `resume.*` 这一族的九个成员里,`resume.usage_window_exhausted`(部署治理窗满)与
259
+ `resume.preflight_rejected`(部署自己的 resume 预检拒)是**唯一带得出「等多久」**的两个:409 体
260
+ additive 携 `retryAfterSec`(秒,`ceil`,下限 1)。这是它们被收成一个闭集的**全部理由**。
261
+ 🔴 **闭集不是「哪些码可以等」的名单**(异源对抗复审第三轮 [medium] 真病修 —— 本段初稿写成
262
+ 「只有这两码可能是『过一会儿行』」,是排他性错断):族内有明确反例 `resume.row_recycling`
263
+ (core 铸文逐字 "this clears on its own; send again in a moment",本包处置 `row-contended`)——
264
+ 它可等,只是 server 给不出秒数。⇒ 新读口返回 `null` 只意味着**没命中这两码**,不意味着「等也没用」。
265
+ 🔴 命中之后也**不等于一定可等** —— 后一码另有一条 `terminal` 臂(行已被结清、token 不可再赎),
266
+ 所以处置不是一格而是**按证据分两格**,见下。
267
+
268
+ 修前:本包的 `classifySubagentResumeFailure` 把两码双双落进开集兜底 `error` ⇒ server 明明给了等待窗,
269
+ 到端只剩一句泛泛失败;而壳侧自己手接了一份判型 —— 判定长在端里,三端各写一遍必然各错一遍
270
+ (与 0.38.0 收 `resume.row_recycling` / `resume.row_gone` 那次同形)。
271
+
272
+ - 新词表(`src/engineErrorCodes.ts`):`RESUME_USAGE_WINDOW_EXHAUSTED` / `RESUME_PREFLIGHT_REJECTED` +
273
+ 闭集 `RESUME_RETRY_LATER_CODES`。**闭的不是「`resume.*` 有几个码」**(那仍是开集),闭的是
274
+ 「server 在哪些码上铸 `retryAfterSec`」;**绝不放宽成 `resume.` 前缀判**。
275
+ 🔴 形制 = **`Object.freeze` 的只读数组,不是 `ReadonlySet`**(异源对抗复审 [medium] 真病修):
276
+ 后者只在类型面只读,运行期就是普通 Set 而判定查的是**同一个实例** ⇒ 公面消费者一行
277
+ `.add('resume.row_gone')` 就能把一条「等也没用」的拒绝翻成带窗的 `retry-later`(实测)。与 #363
278
+ 二轮把 `interactiveHalt.RUN_LEVEL_STOP_ERROR_CODES` 改冻结数组是**同一条已定谳的病形**。
279
+ ⚠️ **同形存量登记(只登记不顺手改)**:同文件 `CONFIG_REFUSAL_CODES` / `DELEGATION_CAP_CODES` /
280
+ `TOOL_END_INTERRUPTED_CODES` 三张表仍是 `ReadonlySet`,同病;它们已在公面上且消费点用 `.has()`
281
+ ⇒ 换形是下游 **BREAKING**(签名变更),不属内容批射程,**另立一批**。
282
+ - 新读口(`src/wireErrorTriage.ts`):`resumeRetryLaterFromError(err)` → `ResumeRetryLaterDetail | null`。
283
+ 结构读不 `instanceof`(客户端是宿主注入的,跨 bundle 的同名类是两个类);键位只认 `errorCode`。
284
+ `retryAfterSec` 的窄读域 = **server 的铸键域**(整数 ∧ ≥1),`0` / 负数 / 小数 / `NaN` / 串一律
285
+ **降缺席**(不降 0)——放行 `0` 就是对消费端说「立刻重试」,而 resume 是 AT-MOST-ONCE 的有副作用动作。
286
+ - 🔴 **「命中本族」不等于「可等」**(异源对抗复审 [medium] 真病修,本批当场改形):
287
+ `resume.preflight_rejected` 在 core 侧有两条臂 —— `retry_later`(行留 pending,同一 token 可再赎)
288
+ 与 `terminal`(行已被单发 expire CAS 结清,**token 不可再赎**),而**判别位在 message 散文里**。
289
+ 仅凭码就宣告「稍后重试」会把一个终局说成暂时等待;而按文案分臂又正是上游改一个词就静默空转的形。
290
+ ⇒ 读口新增**处置位** `waitable`(全包对这个问题的单一判断点),只认**正向证据**:
291
+ `resume.usage_window_exhausted` 恒 `true`(core 铸文的不变量就是证据);`resume.preflight_rejected`
292
+ 只在 server 真给了窗时 `true`。`false` 读作**不可判**,不是「不可重试」。
293
+ - 分类器新增**两格**(`SubagentResumeFailureKind`):有证据的落 `retry-later`,无证据的预检拒落
294
+ `refused-preflight`(端说两臂都成立的那句 —— 「这一拒发生在提交之前,你的决定没被消费」,
295
+ 把「还能不能再赎」交给引擎那行原文)。判决与 `resumeSettledSubagent()` 的失败臂 additive 携
296
+ `code?` 与 `retryAfterSec?`;**既有各格零挪位**,本层照旧**一格都不重试**。
297
+ - `classifySubagentResumeFailure` 的返回位由两成员内联形抽成具名 `SubagentResumeFailureVerdict`
298
+ (结构逐字兼容,TS 结构化类型 ⇒ 消费端零差异)。
299
+
300
+ ### 登记 —— SDK 型面缺口([C233]①,只记账不改码)
301
+
302
+ SDK 8.1.0 的 `ToolApprovalsResource.respond` 体型只锚到 `persistRule: { rule: string }`,server ≥7.46.0
303
+ 已发的 `edited` / `batchOfferIndex` 两位没跟;同族锚滞后还有 `ToolApprovalFrame.ruleOffers` /
304
+ `PendingCheckpoint.ruleOffers` 与 ack 的三个回显位。🔴 **本包侧零 cast、也不需要 cast** —— 本包不铸
305
+ respond 的 wire 体(`RespondToolApprovalFn` 是宿主注入的函数型,包只交扁平兄弟位),吃这个缺口的是
306
+ 三端的注入面。登记进 `docs/INTEGRATION-CLIENTS.md` §7d **P-44**,正位解在 sdk。
307
+
308
+ ### 门(三件同批)
309
+
310
+ - 新门 `scripts/run-rule-offers-reader-test.mjs`(109 判据):公面在场(d.ts 导出形)· 两代键各一正控 ·
311
+ 逐条丢坏且原始下标不前移(头/中/尾三个方向 × ****十九条丢坏路径各走一遍** —— 坏 single 两形 / 坏 batch 十三形(rules 非数组·空·超帽 + 成员十形:缺 segment / rule 空串·非串 / match 表外词 / 缺 command·空串 / segment 空串·非串 / 成员 null·undefined·非对象)/ 非对象 / null / 不认识的 kind,外加旧键归一臂七形 —— **后两族专门够 `readRuleTriple` 自己那两道守卫**(外层的 null/非对象过滤在 offer 级就把它们拦掉了,成员级与旧键项才喂得到);三轮变异实发的逃逸全在这一格上:只拿「不认识的 kind」当坏条时读器里另外几条 `continue` 改成整只 `return undefined` 照样全绿、成员窄化换成裸 cast 照样全绿、删掉 `readRuleTriple` 的载体守卫照样全绿(而那一形会当场抛 TypeError))· batch 下标语义与成员不逐条丢 · `uncoveredSegments`
312
+ 六种坏值 · 空数组/非数组/超帽/全坏 · 两代取舍序(含 `null` ≡ 缺席的明示裁定)· **单一铸点对账**
313
+ (公面读口与活卡帧腿/durable 行腿两条卡端口出口对同一份素材逐字相等)· 纯函数不改写入参。
314
+ - 新门 `scripts/run-resume-retry-later-test.mjs`(115 判据):两码正控(带窗/不带窗)· 闭集恰两员 ·
315
+ 负控 16 形(同族七个「等也没用」的码、429 同名不同门、两个下划线族、文本引用、坏载体)·
316
+ `retryAfterSec` 十一种脏值消毒 + 下边界 1 · `waitable` 的证据判据(治理窗满恒 true / 无窗预检拒
317
+ 必须 false / 脏窗消毒后不许升格)· **闭集运行期改不动**(试改 + 试改后判定没漂,两半都断言)·
318
+ 分类器两格分立与既有各格零挪位 · 未知码上的窗不被采信 · **`null` ≠「等也没用」的可执行反钉**
319
+ (同族 `resume.row_recycling` 在读口上是 `null`、在分类器上却必须落可等的 `row-contended`)·
320
+ 端到端三形(带窗 / 无窗预检拒 / 「等也没用」的码)且**一格都不重试**。
321
+ - 两门同批登记 `scripts/gates-manifest.json` + README `Guards` 表(公开文案)。
322
+ - 登记面:`scripts/public-export-baseline.json` 815→821(六个新公面名)· `docs/refactor/p1-scan/singleton-manifest.json` 279→280(`RESUME_RETRY_LATER_CODES` 只读词表,dupRisk low,high 上限不动)·
323
+ `scripts/run-client-core-typeshape-test.mjs` 的 `unknownExport` 棘轮 268→272(四个边界读口入参位,
324
+ 逐条登记理由;**B4 恒 20、裸 unknown 返回恒 0 不动**)· `docs/INTEGRATION-CLIENTS.md` §0a/§2/§2b 计数。
325
+
326
+ ### 🔴 老宿主必读
327
+
328
+ `SubagentResumeFailureKind` 从八员变**十**员(`retry-later` + `refused-preflight`):
329
+ **结构形免动**(读 `reason`/`detail` 的宿主一字不用改),
330
+ 但对该联合做**穷尽 switch**(`never` 兜底)的宿主提货时会编译红 —— 那是设计:一个新的处置落进
331
+ `default` 而无人处理,就是把「等一会儿就好」渲成「失败了」。同 0.55.0 `RetryStatus` 那条老宿主纪律。
332
+
333
+ ### 已知局限(本版新增)
334
+
335
+ - **本包不铸 `ResumeRetryLaterError`,也不 `instanceof` 它**:该类是 SDK 8.1.0 的,而本包 peer 地板
336
+ 仍是 `>=7.4.0`。挂在 ≤8.0 SDK 上的宿主,这两码走的是无字段的族基类 ⇒ `retryAfterSec` 在错误对象上
337
+ 根本不存在,本包如实报缺席(**缺席 ≠ 0**)。
338
+ - **`resume.preflight_rejected` 的两条上游臂(`retry_later` / `terminal`)在客户端分辨不出来** ——
339
+ 判别位在 message 散文里,本包**不按文案分臂**,改按**正向证据**分格(见上)。所以 `refused-preflight`
340
+ 这一格的语义是**不可判**而不是「不可重试」:端只说两臂都成立的那句,「还能不能再赎」交给引擎那行
341
+ 原文。**候上游给机读判别位**(有了它这一格就能再分成两格,届时是 additive 加员)。
342
+
343
+ ---
344
+
52
345
  ## 0.56.0(2026-09-05)
53
346
 
54
347
  > 发包批阶段一(2026-09-05,主会话):`package.json` 0.55.0→0.56.0、README Version 行、本段头转日期形、`FROZEN` pending 行同批;内容由 1.0.100 提货车 B 在隔离树交付,主会话收货补包侧门与 eventId 修钉(见「门与收货修钉」)。
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.56.0
38
+ **Version:** 0.58.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.2.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.
@@ -265,6 +267,9 @@ public-surface guard checks that last one).
265
267
  | `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
268
  | `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
269
  | `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 |
270
+ | `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 |
271
+ | `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 |
272
+ | `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 |
268
273
 
269
274
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
270
275
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -721,6 +721,25 @@ export function eventToSdkMessage(ev, ctx) {
721
721
  */
722
722
  case 'approval_request':
723
723
  return dropped('unsupported_arm', 'approval_request');
724
+ /**
725
+ * `approval_revoke`(design/172 撤卡帧;server ≥7.55.0 起也进重放,SDK 8.x 才把它写进
726
+ * `AgentEvent` union —— 0.58.0 sdk 8.2.0 提货批**编译期逼进来**的臂,与 `engine_notice`
727
+ * / `human_input` 走的是同一条路:臂一进 union,`default` 的 B5 穷举断言当场红)。
728
+ *
729
+ * 🔴 **与 `approval_request` 同源同处置,所以并**在它旁边而不是并进上面那两条 —— 它是
730
+ * 同一条流内审批协议的**撤销**半场(载荷同是 `ApprovalFrameEnvelope`,`schemaVersion`
731
+ * 未知的帧要按信封先读再窄化)。本包今天**零消费口**:`isToolApprovalFrame` 只认
732
+ * legacy 的 `tool_approval` / `tool_approval_complete` 两词,新族两帧一个都不接。
733
+ * 🔴 分类 = `dropped/unsupported_arm` 而**不是** `nothing/not_in_slice`(与
734
+ * `approval_request` 同一条理由,逐字见上一臂):`none` 是静默的,只有 `dropped` 会经
735
+ * `reportDroppedFrame` 走宿主 sink + console 留痕。撤卡帧带的是**用户可见的状态迁移**
736
+ * (那张卡已经不该再被决断了),悄悄丢掉它比吼一行更坏。
737
+ * 📋 缺口如实记账(**不**在本批修 —— 与 `approval_request` 是同一条新卡链,属功能不属修复):
738
+ * 接法在上一臂的「接法」段里写着,撤卡半场按 `approvalId` 撤同一张卡;在那条链落地之前
739
+ * **不许**由本切片顺手编一个假 transcript 形来假装接上了。
740
+ */
741
+ case 'approval_revoke':
742
+ return dropped('unsupported_arm', 'approval_revoke');
724
743
  // ── `engine_notice`(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)────
725
744
  // 🔴 **到期复核已兑现(sdk 7.4.0 提货,0.48.0)** —— 与 `human_input`(sdk 6.9.0)逐字同一条路。
726
745
  // 本臂此前是 switch **之前**的一条 raw 预分派,理由 = 它还没进已发布 SDK 的 `AgentEvent`
@@ -180,6 +180,65 @@ export declare const RESUME_AT_ERROR_CODE_PREFIX = "resume_at.";
180
180
  export declare const REWIND_ERROR_CODE_PREFIXES: readonly ["resume_at.", "rewind_snapshot."];
181
181
  /** 该码是否属 rewind/resume 可自解族。缺席 ⇒ false。 */
182
182
  export declare function isRewindFamilyCode(code: string | undefined): boolean;
183
+ /**
184
+ * `resume.usage_window_exhausted`(#449 G1,core 5.60.1;server ≥7.47.0)—— 这一行的账本键上,
185
+ * 本部署的**治理窗**满了。core 铸文的两句不变量:**什么都没消费、什么都没解钉** ⇒ 同一个 token
186
+ * 带同一个决议在窗放开后可**直兑**(所以处置是「等」,不是「重开」也不是「改配置」)。
187
+ * ⚠️ 与 429 的 {@link USAGE_WINDOW_EXHAUSTED}(`usage.window_exhausted`,提交面 pre-admission)
188
+ * **同一本账、不同门、不同码**:本码是 resume/decide 腿的 pre-CAS 拒。别把两者合并判。
189
+ */
190
+ export declare const RESUME_USAGE_WINDOW_EXHAUSTED = "resume.usage_window_exhausted";
191
+ /**
192
+ * `resume.preflight_rejected`(#376,core 5.65 retry-later 形;server ≥7.51.0)—— 部署自己的
193
+ * `RunnerDeps.resumePreflight` 拒了这次 resume(显式拒 / 抛 / 超时 / 答案读不动,四臂一律
194
+ * fail-closed)。它是 CAS 前的**最后一档**,所以什么都没被消费。
195
+ * 🔴 **core 侧另有一条 `terminal` 臂**(行已被这次拒绝的单发 expire CAS 结清、token 不可再赎),
196
+ * 而两臂的**判别位在 message 散文里**。本包**不按文案分臂** —— 按文案分支正是上游改一个词就
197
+ * 静默空转的形。⇒ 消费端能诚实说的只有两臂都成立的那句:「这一拒发生在提交之前,你的决定
198
+ * 没被消费」;「还能不能再赎」交给引擎那行原文去说,别替它下结论。
199
+ */
200
+ export declare const RESUME_PREFLIGHT_REJECTED = "resume.preflight_rejected";
201
+ /**
202
+ * `resume.placement_mismatch`(#376 / core 5.65,design/380 O1③;server ≥7.51.0)—— 这次 resume
203
+ * 显式带的 `internals.placementRoot` 与 suspend 铸点记下的 `CheckpointState.placementRootSessionId`
204
+ * **不同**。目标绑定的 env factory 按那个固定点查 placement,静默换根会把这条腿(以及它往下传的
205
+ * 每一个子代)重新落在**另一台目标**上,而 park 的工作区在原来那台 ⇒ **拒在 CAS 之前**,行仍 `pending`。
206
+ *
207
+ * 🔴 **它不在 {@link RESUME_RETRY_LATER_CODES} 里,而且不该在**:server 只在
208
+ * `resume.usage_window_exhausted` / `resume.preflight_rejected` 两码上铸 `retryAfterSec`
209
+ * (server 7.51.0 `dist/http/server.js` 的 `CheckpointError` 出口逐字),本码恒无窗;更要紧的是
210
+ * **等一会儿对它毫无用处** —— 出路是「用记录里的那个 root 重来,或干脆不传 `internals.placementRoot`
211
+ * 继承记录值」,是**换参数**不是**等时间**。把它放进时间性闭集会让人白等一个永远不会自己好的拒绝。
212
+ * ⇒ 它与 `resume.preflight_rejected` 一起构成**另一个**闭集:{@link import('./resumeRefusalCopy.js').RESUME_REFUSAL_CODES}
213
+ * (「有人话可补」的拒绝),两个闭集刻意分家 —— 一个回答「能不能等」,一个回答「该对人说什么」。
214
+ */
215
+ export declare const RESUME_PLACEMENT_MISMATCH = "resume.placement_mismatch";
216
+ /**
217
+ * 时间性拒绝族的**闭集**。
218
+ *
219
+ * 🔴 这是本文件的第二个闭集,但闭的**不是**「`resume.*` 一共有几个码」(那仍是开集,新码照旧
220
+ * 落 {@link isRewindFamilyCode} 之外的开集兜底),闭的是「**server 在哪些码上铸 `retryAfterSec`**」:
221
+ * server 的铸键判据逐字 =「本码 ∧ 有限正数」,两码之外恒缺席;SDK 8.1.0 `classifyApiError` 同样
222
+ * 按这**两个具名码**铸 `ResumeRetryLaterError`(具名分支排在 `resume.` 前缀兜底**之前**)。
223
+ * 🔴 **绝不放宽成 `resume.` 前缀判**:那会把 `retain_off` / `evicted` / `row_gone` 这些**等也没用**
224
+ * 的码一起说成「过会儿再试」—— 一半用户白等,另一半白重开(与 `row_recycling`/`row_gone` 禁合并
225
+ * 同一条纪律)。加成员 = 上游真在新码上铸了 `retryAfterSec`,必须同批带判据。
226
+ *
227
+ * 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源对抗复审 [medium] 采纳,真病;
228
+ * 与 `interactiveHalt.RUN_LEVEL_STOP_ERROR_CODES`(#363 二轮)**同一条已定谳的病形**):
229
+ * `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
230
+ * 实例**。任何 JS 消费者(公面上它是导出的)`.add('resume.row_gone')` 之后,一条「等也没用」的
231
+ * 拒绝就会当场变成带窗的 `retry-later`(实测:`row_gone` + `retryAfterSec:30` 从 `row-gone` 翻成
232
+ * `retry-later`)—— 闭集与「只认正向证据」两道约束一起被绕过。冻结数组在**运行期**真的改不动
233
+ * (ESM 恒 strict:`push`/下标赋值直接抛),于是「公开面」与「判定源」可以安全地是同一个物。
234
+ * ⚠️ 判据形随之从 `.has()` 改成 `.includes()`(与 `parkResolver.GATE_FAILURE_CODES` 同姿势;
235
+ * 闭集只有两员,查找成本不是这里的量)。
236
+ * ⚠️ **同形存量登记**(只登记不顺手改):同文件的 `CONFIG_REFUSAL_CODES` / `DELEGATION_CAP_CODES` /
237
+ * `TOOL_END_INTERRUPTED_CODES` 三张表今天仍是 `ReadonlySet`,同病。它们**已经在公面上**且消费点
238
+ * 用 `.has()` ⇒ 换形是下游 BREAKING(签名从 `ReadonlySet<string>` 变 `readonly string[]`),
239
+ * 不属内容批射程;本条按现状登记,换形另立一批。本位是**新铸**的,所以在出生那天就用对形。
240
+ */
241
+ export declare const RESUME_RETRY_LATER_CODES: readonly string[];
183
242
  /**
184
243
  * server 温切 drain 门的 pre-stream 拒收码(503 + `errorCode:"draining"`;server 侧
185
244
  * `error:"draining"` 是冻结契约,SDK toApiError 盖成 `errorCode`)。此前壳/包注释各持裸字面。
@@ -246,6 +246,85 @@ export const REWIND_ERROR_CODE_PREFIXES = [RESUME_AT_ERROR_CODE_PREFIX, 'rewind_
246
246
  export function isRewindFamilyCode(code) {
247
247
  return typeof code === 'string' && REWIND_ERROR_CODE_PREFIXES.some((p) => code.startsWith(p));
248
248
  }
249
+ // ── resume 族的**时间性拒绝**二码(L-102;server ≥7.47.0 / ≥7.51.0,SDK 8.1.0 `ResumeRetryLaterError`)──
250
+ //
251
+ // ⚠️ 与上面的 `resume_at.`(**下划线**,rewind 取址族)是**两族** —— 这里是 `resume.`(**点**,
252
+ // design/122 D2 的 409 合同拒绝族)。同一个词在两个族里,判别靠分隔符,别按 `resume` 子串猜。
253
+ //
254
+ // 语义:下面两码是这一族里**唯一携带可执行等待量**(`retryAfterSec`)的两码 —— 这就是它们成为
255
+ // 一个闭集的**全部理由**。
256
+ // 🔴 **本闭集回答的不是「哪些码可以等」**(异源对抗复审 [medium] 真病修:上一版这段写成「只有下面
257
+ // 两码可能是『现在不行、过一会儿行』」,是**排他性错断**)。族内**明确的反例**就在本文件视野内:
258
+ // `resume.row_recycling` 的 core 铸文逐字「this clears on its own; send again in a moment」——
259
+ // 它可等,只是 server 给不出秒数,所以它**不在**本闭集里、也**不该**在。
260
+ // ⇒ 「本读口返回 `null`」只意味着**没命中这两码**,绝不意味着「等也没用」;闭集外的码照旧走
261
+ // `classifySubagentResumeFailure` 的既有各格(`row-contended` 就是可等的那一格)。
262
+ // 🔴 命中之后**也不等于一定可等**:后一码另有一条 `terminal` 臂(见该码顶注)⇒ 处置由
263
+ // `wireErrorTriage` 的 `waitable` 按**正向证据**判,不由「命中本族」判。此前它们双双落进 `classifySubagentResumeFailure`
264
+ // 的开集兜底 `error` ⇒ server 明明给了「等多久」,到客户端只剩一句泛泛失败(与 0.38.0 收
265
+ // `row_recycling`/`row_gone` 那次同形)。
266
+ /**
267
+ * `resume.usage_window_exhausted`(#449 G1,core 5.60.1;server ≥7.47.0)—— 这一行的账本键上,
268
+ * 本部署的**治理窗**满了。core 铸文的两句不变量:**什么都没消费、什么都没解钉** ⇒ 同一个 token
269
+ * 带同一个决议在窗放开后可**直兑**(所以处置是「等」,不是「重开」也不是「改配置」)。
270
+ * ⚠️ 与 429 的 {@link USAGE_WINDOW_EXHAUSTED}(`usage.window_exhausted`,提交面 pre-admission)
271
+ * **同一本账、不同门、不同码**:本码是 resume/decide 腿的 pre-CAS 拒。别把两者合并判。
272
+ */
273
+ export const RESUME_USAGE_WINDOW_EXHAUSTED = 'resume.usage_window_exhausted';
274
+ /**
275
+ * `resume.preflight_rejected`(#376,core 5.65 retry-later 形;server ≥7.51.0)—— 部署自己的
276
+ * `RunnerDeps.resumePreflight` 拒了这次 resume(显式拒 / 抛 / 超时 / 答案读不动,四臂一律
277
+ * fail-closed)。它是 CAS 前的**最后一档**,所以什么都没被消费。
278
+ * 🔴 **core 侧另有一条 `terminal` 臂**(行已被这次拒绝的单发 expire CAS 结清、token 不可再赎),
279
+ * 而两臂的**判别位在 message 散文里**。本包**不按文案分臂** —— 按文案分支正是上游改一个词就
280
+ * 静默空转的形。⇒ 消费端能诚实说的只有两臂都成立的那句:「这一拒发生在提交之前,你的决定
281
+ * 没被消费」;「还能不能再赎」交给引擎那行原文去说,别替它下结论。
282
+ */
283
+ export const RESUME_PREFLIGHT_REJECTED = 'resume.preflight_rejected';
284
+ /**
285
+ * `resume.placement_mismatch`(#376 / core 5.65,design/380 O1③;server ≥7.51.0)—— 这次 resume
286
+ * 显式带的 `internals.placementRoot` 与 suspend 铸点记下的 `CheckpointState.placementRootSessionId`
287
+ * **不同**。目标绑定的 env factory 按那个固定点查 placement,静默换根会把这条腿(以及它往下传的
288
+ * 每一个子代)重新落在**另一台目标**上,而 park 的工作区在原来那台 ⇒ **拒在 CAS 之前**,行仍 `pending`。
289
+ *
290
+ * 🔴 **它不在 {@link RESUME_RETRY_LATER_CODES} 里,而且不该在**:server 只在
291
+ * `resume.usage_window_exhausted` / `resume.preflight_rejected` 两码上铸 `retryAfterSec`
292
+ * (server 7.51.0 `dist/http/server.js` 的 `CheckpointError` 出口逐字),本码恒无窗;更要紧的是
293
+ * **等一会儿对它毫无用处** —— 出路是「用记录里的那个 root 重来,或干脆不传 `internals.placementRoot`
294
+ * 继承记录值」,是**换参数**不是**等时间**。把它放进时间性闭集会让人白等一个永远不会自己好的拒绝。
295
+ * ⇒ 它与 `resume.preflight_rejected` 一起构成**另一个**闭集:{@link import('./resumeRefusalCopy.js').RESUME_REFUSAL_CODES}
296
+ * (「有人话可补」的拒绝),两个闭集刻意分家 —— 一个回答「能不能等」,一个回答「该对人说什么」。
297
+ */
298
+ export const RESUME_PLACEMENT_MISMATCH = 'resume.placement_mismatch';
299
+ /**
300
+ * 时间性拒绝族的**闭集**。
301
+ *
302
+ * 🔴 这是本文件的第二个闭集,但闭的**不是**「`resume.*` 一共有几个码」(那仍是开集,新码照旧
303
+ * 落 {@link isRewindFamilyCode} 之外的开集兜底),闭的是「**server 在哪些码上铸 `retryAfterSec`**」:
304
+ * server 的铸键判据逐字 =「本码 ∧ 有限正数」,两码之外恒缺席;SDK 8.1.0 `classifyApiError` 同样
305
+ * 按这**两个具名码**铸 `ResumeRetryLaterError`(具名分支排在 `resume.` 前缀兜底**之前**)。
306
+ * 🔴 **绝不放宽成 `resume.` 前缀判**:那会把 `retain_off` / `evicted` / `row_gone` 这些**等也没用**
307
+ * 的码一起说成「过会儿再试」—— 一半用户白等,另一半白重开(与 `row_recycling`/`row_gone` 禁合并
308
+ * 同一条纪律)。加成员 = 上游真在新码上铸了 `retryAfterSec`,必须同批带判据。
309
+ *
310
+ * 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源对抗复审 [medium] 采纳,真病;
311
+ * 与 `interactiveHalt.RUN_LEVEL_STOP_ERROR_CODES`(#363 二轮)**同一条已定谳的病形**):
312
+ * `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
313
+ * 实例**。任何 JS 消费者(公面上它是导出的)`.add('resume.row_gone')` 之后,一条「等也没用」的
314
+ * 拒绝就会当场变成带窗的 `retry-later`(实测:`row_gone` + `retryAfterSec:30` 从 `row-gone` 翻成
315
+ * `retry-later`)—— 闭集与「只认正向证据」两道约束一起被绕过。冻结数组在**运行期**真的改不动
316
+ * (ESM 恒 strict:`push`/下标赋值直接抛),于是「公开面」与「判定源」可以安全地是同一个物。
317
+ * ⚠️ 判据形随之从 `.has()` 改成 `.includes()`(与 `parkResolver.GATE_FAILURE_CODES` 同姿势;
318
+ * 闭集只有两员,查找成本不是这里的量)。
319
+ * ⚠️ **同形存量登记**(只登记不顺手改):同文件的 `CONFIG_REFUSAL_CODES` / `DELEGATION_CAP_CODES` /
320
+ * `TOOL_END_INTERRUPTED_CODES` 三张表今天仍是 `ReadonlySet`,同病。它们**已经在公面上**且消费点
321
+ * 用 `.has()` ⇒ 换形是下游 BREAKING(签名从 `ReadonlySet<string>` 变 `readonly string[]`),
322
+ * 不属内容批射程;本条按现状登记,换形另立一批。本位是**新铸**的,所以在出生那天就用对形。
323
+ */
324
+ export const RESUME_RETRY_LATER_CODES = Object.freeze([
325
+ RESUME_USAGE_WINDOW_EXHAUSTED,
326
+ RESUME_PREFLIGHT_REJECTED,
327
+ ]);
249
328
  // ── drain / 场景执法族(A-028.11/.13 单源化,#244 族E,2026-08-15)────────────────────────────
250
329
  /**
251
330
  * server 温切 drain 门的 pre-stream 拒收码(503 + `errorCode:"draining"`;server 侧