@sema-agent/client-core 0.55.0 → 0.57.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.
@@ -246,6 +246,70 @@ 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
+ * 时间性拒绝族的**闭集**。
286
+ *
287
+ * 🔴 这是本文件的第二个闭集,但闭的**不是**「`resume.*` 一共有几个码」(那仍是开集,新码照旧
288
+ * 落 {@link isRewindFamilyCode} 之外的开集兜底),闭的是「**server 在哪些码上铸 `retryAfterSec`**」:
289
+ * server 的铸键判据逐字 =「本码 ∧ 有限正数」,两码之外恒缺席;SDK 8.1.0 `classifyApiError` 同样
290
+ * 按这**两个具名码**铸 `ResumeRetryLaterError`(具名分支排在 `resume.` 前缀兜底**之前**)。
291
+ * 🔴 **绝不放宽成 `resume.` 前缀判**:那会把 `retain_off` / `evicted` / `row_gone` 这些**等也没用**
292
+ * 的码一起说成「过会儿再试」—— 一半用户白等,另一半白重开(与 `row_recycling`/`row_gone` 禁合并
293
+ * 同一条纪律)。加成员 = 上游真在新码上铸了 `retryAfterSec`,必须同批带判据。
294
+ *
295
+ * 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源对抗复审 [medium] 采纳,真病;
296
+ * 与 `interactiveHalt.RUN_LEVEL_STOP_ERROR_CODES`(#363 二轮)**同一条已定谳的病形**):
297
+ * `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
298
+ * 实例**。任何 JS 消费者(公面上它是导出的)`.add('resume.row_gone')` 之后,一条「等也没用」的
299
+ * 拒绝就会当场变成带窗的 `retry-later`(实测:`row_gone` + `retryAfterSec:30` 从 `row-gone` 翻成
300
+ * `retry-later`)—— 闭集与「只认正向证据」两道约束一起被绕过。冻结数组在**运行期**真的改不动
301
+ * (ESM 恒 strict:`push`/下标赋值直接抛),于是「公开面」与「判定源」可以安全地是同一个物。
302
+ * ⚠️ 判据形随之从 `.has()` 改成 `.includes()`(与 `parkResolver.GATE_FAILURE_CODES` 同姿势;
303
+ * 闭集只有两员,查找成本不是这里的量)。
304
+ * ⚠️ **同形存量登记**(只登记不顺手改):同文件的 `CONFIG_REFUSAL_CODES` / `DELEGATION_CAP_CODES` /
305
+ * `TOOL_END_INTERRUPTED_CODES` 三张表今天仍是 `ReadonlySet`,同病。它们**已经在公面上**且消费点
306
+ * 用 `.has()` ⇒ 换形是下游 BREAKING(签名从 `ReadonlySet<string>` 变 `readonly string[]`),
307
+ * 不属内容批射程;本条按现状登记,换形另立一批。本位是**新铸**的,所以在出生那天就用对形。
308
+ */
309
+ export const RESUME_RETRY_LATER_CODES = Object.freeze([
310
+ RESUME_USAGE_WINDOW_EXHAUSTED,
311
+ RESUME_PREFLIGHT_REJECTED,
312
+ ]);
249
313
  // ── drain / 场景执法族(A-028.11/.13 单源化,#244 族E,2026-08-15)────────────────────────────
250
314
  /**
251
315
  * server 温切 drain 门的 pre-stream 拒收码(503 + `errorCode:"draining"`;server 侧
@@ -1087,6 +1087,54 @@ export declare function readToolApprovalRespondRefusal(err: unknown): ToolApprov
1087
1087
  * `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
1088
1088
  * 结构读(不依赖类型收窄),因为它同时服务 raw SSE 与 durable 回放两条入口。 */
1089
1089
  export declare function isToolApprovalFrame(ev: unknown): ev is ToolApprovalFrame;
1090
+ /**
1091
+ * `ruleOffers`(server ≥7.46.0 的判别联合)的结构读。
1092
+ *
1093
+ * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改)。此前它只经
1094
+ * **卡端口**({@link ApprovalCardRequest.ruleOffers})出包 —— 不走卡端口架构的宿主(浏览器端没有
1095
+ * Ink 三选卡,自己拿帧渲)只能在自己那边**重铸一遍**同一把窄读器,而这把窄读器承载的是
1096
+ * **兑付安全**判据(原始下标不前移、逐条丢坏、闭集 kind),重铸一次 = 多一份会各自漂的判官。
1097
+ * ⇒ 公面出口是「判定归包、呈现归端」在这一条腿上的兑现,不是便利函数。
1098
+ * 🔴 **两代 wire 键请走 {@link readRuleOfferSupply}**:本函数只读**新键**(server ≥7.46.0 的
1099
+ * `ruleOffers`);退役键 `ruleSuggestions`(server ≤7.45)的归一在那一口,两键的取舍序也在那里
1100
+ * (新键在场即定局,绝不混编)。手里只有新键才直接用本口。
1101
+ * 🔴 **`offerIndex` 的定义域随腿不同**,消费前必读 {@link RuleOffer} 顶注:活卡帧腿上它是合法
1102
+ * **选择键**(可当 `persistRule.batchOfferIndex` 回兑),durable 行腿上它只是展示/对账座
1103
+ * (server `boundedRuleOffers` 已压紧过一次)——**本函数不知道调用方在哪条腿上**,分辨是调用方的事。
1104
+ *
1105
+ * 🔴 **逐条丢坏、原始下标不前移**:坏 offer 逐条丢弃(一条坏的不该让另一条真的消失,与 server
1106
+ * `boundedRuleOffers` 同向),但留下来的每一条都带**原始 wire 下标** {@link RuleOffer.offerIndex} ——
1107
+ * core 的契约原话:做不到保住原始下标的消费端「must suppress its persistence actions entirely」,
1108
+ * 因为 `batch` 臂的兑付键就是下标。压紧 = 人点的第 k 个与服务端兑的第 k 个指向两条不同规则。
1109
+ * 🔴 `kind` 是**闭集判别位**:不认识的 kind ⇒ 丢这一条(不猜、不降级成 single)。
1110
+ * 全部不合形/非数组/超帽 ⇒ 整体缺席(卡不渲「不再询问」档)。
1111
+ */
1112
+ export declare function readRuleOffers(v: unknown): RuleOffer[] | undefined;
1113
+ /**
1114
+ * 两代 wire 供给 → **包内单一形**(#334/[5223],0.43.0):新键优先,新键整只读不出来才看旧键。
1115
+ *
1116
+ * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改;0.56.0 及更早的
1117
+ * 内部名是 `readOfferSupply`,**纯改名**没有第二个消费点)。宿主手里拿到的是**一整帧/一整行**,
1118
+ * 上面同时可能有 `ruleOffers`(新)与 `ruleSuggestions`(旧)两个键 —— 这一口是三端唯一该调的那个:
1119
+ * `readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions)`。
1120
+ * 两代键的取舍序是**判据不是便利**(见下面两段红条),端各写一遍必然在 `null` 那一格上各错一遍。
1121
+ *
1122
+ * 🔴 **新键在场即定局,绝不混编**:新键**有载体**而读出空(数组在但全条坏形、或压根不是数组)
1123
+ * 也**不**回落旧键 —— 一台 7.46 引擎不会同时按两代形铸候选,拿旧键顶上去等于把一份异源素材
1124
+ * 冒充成这次 ask 的候选。
1125
+ *
1126
+ * 🔴 **`null` 与 `undefined` 同视为「新键缺席」**(异源对抗复审 [medium] 追问后的**明示裁定**,
1127
+ * 不是漏判):判据是**代价不对称**——
1128
+ * · 认 null 为缺席的失效面:一台 **7.46** 引擎把新键发成 `null` **且**同时发了旧键。这不可能
1129
+ * 发生:7.46 的三腿上旧键一个字都不铸(engine fixture 直证,同文件 0 命中)⇒ 回落读到的
1130
+ * 只会是 `undefined`,结果与「整只缺席」逐字节相同,没有异源素材可混;
1131
+ * · 认 null 为坏形的失效面:任何把「缺席」序列化成 `null` 的中转层(JSON 规范化包装器、
1132
+ * 某些 SQL/JSONB 读面、mock)会让**所有 ≤7.45 引擎**的「不再询问」档整段消失 —— 那正是
1133
+ * [5223] 这一批要修的病本身,只是换了个触发条件。
1134
+ * ⇒ 取「null == 缺席」。安全面上它**不新增**任何攻击面:一个能塞 `{ruleOffers:null, ruleSuggestions:[…]}`
1135
+ * 的注入面,同样能只塞 `{ruleSuggestions:[…]}`,而后者为了兼容 7.44 本来就必须收。
1136
+ */
1137
+ export declare function readRuleOfferSupply(offers: unknown, legacy: unknown): RuleOffer[] | undefined;
1090
1138
  /** 帧腿的宿主车道参数(#229 respond-note 批,0.29.0)。 */
1091
1139
  export interface ToolApprovalFrameLaneOpts {
1092
1140
  /**
@@ -271,13 +271,13 @@ parkGatedCallId) {
271
271
  // [C170] 答问②半场(0.29.0):durable 富行的两个展示键随卡透传 —— 修前这里是四位闭集,
272
272
  // server 7.16.0 起行上就有的 ruleSuggestions/governanceForced 在「行 → 卡」重铸处整段丢失
273
273
  // (feed 行原样透传零丢失,丢的只有这处)。governanceForced 条件 stamp 只认 === true(缺席
274
- // 纪律与活卡腿同款:缺席=无治理来源证据,绝不写 false);候选走 readOfferSupply 同一把
274
+ // 纪律与活卡腿同款:缺席=无治理来源证据,绝不写 false);候选走 readRuleOfferSupply 同一把
275
275
  // 合形窄化,落**只读键**(红线见 ApprovalCardRequest.ruleOffersReadOnly 顶注:/decide
276
276
  // 无规则位,落可兑付位=假 affordance)。
277
277
  // #334(0.43.0):行上的活键换成 `ruleOffers`(server ≥7.46.0 `PendingCheckpoint.ruleOffers`);
278
278
  // SDK 7.2.0 的 `PendingCheckpoint` 声明尚无此键 ⇒ **结构视图读**(与本函数下方 `riskDescriptor.probeCause`
279
279
  // 同款姿势),旧键 `ruleSuggestions` 保读兼容(7.44 及更旧引擎仍在场),两键经同一把窄读器归一。
280
- const ruleOffersReadOnly = readOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
280
+ const ruleOffersReadOnly = readRuleOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
281
281
  // #280 件1(0.30.4):durable 行腿的探针因由载体 = `riskDescriptor.probeCause`(server 对
282
282
  // riskDescriptor 整体透传,与活卡帧同值)。SDK 6.17.2 的 riskDescriptor 声明尚无此键 ⇒ 结构
283
283
  // 视图读(与 caps 防御读同款姿势),类型半场候 SDK 班车;载体形不合 ⇒ 不铸键(内部结构不校,
@@ -842,6 +842,18 @@ const MAX_RULE_OFFER_BATCH_MEMBERS_TOLERATED = 8;
842
842
  /**
843
843
  * `ruleOffers`(server ≥7.46.0 的判别联合)的结构读。
844
844
  *
845
+ * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改)。此前它只经
846
+ * **卡端口**({@link ApprovalCardRequest.ruleOffers})出包 —— 不走卡端口架构的宿主(浏览器端没有
847
+ * Ink 三选卡,自己拿帧渲)只能在自己那边**重铸一遍**同一把窄读器,而这把窄读器承载的是
848
+ * **兑付安全**判据(原始下标不前移、逐条丢坏、闭集 kind),重铸一次 = 多一份会各自漂的判官。
849
+ * ⇒ 公面出口是「判定归包、呈现归端」在这一条腿上的兑现,不是便利函数。
850
+ * 🔴 **两代 wire 键请走 {@link readRuleOfferSupply}**:本函数只读**新键**(server ≥7.46.0 的
851
+ * `ruleOffers`);退役键 `ruleSuggestions`(server ≤7.45)的归一在那一口,两键的取舍序也在那里
852
+ * (新键在场即定局,绝不混编)。手里只有新键才直接用本口。
853
+ * 🔴 **`offerIndex` 的定义域随腿不同**,消费前必读 {@link RuleOffer} 顶注:活卡帧腿上它是合法
854
+ * **选择键**(可当 `persistRule.batchOfferIndex` 回兑),durable 行腿上它只是展示/对账座
855
+ * (server `boundedRuleOffers` 已压紧过一次)——**本函数不知道调用方在哪条腿上**,分辨是调用方的事。
856
+ *
845
857
  * 🔴 **逐条丢坏、原始下标不前移**:坏 offer 逐条丢弃(一条坏的不该让另一条真的消失,与 server
846
858
  * `boundedRuleOffers` 同向),但留下来的每一条都带**原始 wire 下标** {@link RuleOffer.offerIndex} ——
847
859
  * core 的契约原话:做不到保住原始下标的消费端「must suppress its persistence actions entirely」,
@@ -849,7 +861,7 @@ const MAX_RULE_OFFER_BATCH_MEMBERS_TOLERATED = 8;
849
861
  * 🔴 `kind` 是**闭集判别位**:不认识的 kind ⇒ 丢这一条(不猜、不降级成 single)。
850
862
  * 全部不合形/非数组/超帽 ⇒ 整体缺席(卡不渲「不再询问」档)。
851
863
  */
852
- function readRuleOffers(v) {
864
+ export function readRuleOffers(v) {
853
865
  if (!Array.isArray(v))
854
866
  return undefined;
855
867
  if (v.length === 0 || v.length > MAX_RULE_OFFERS_TOLERATED)
@@ -923,6 +935,12 @@ function readLegacyRuleSuggestions(v) {
923
935
  /**
924
936
  * 两代 wire 供给 → **包内单一形**(#334/[5223],0.43.0):新键优先,新键整只读不出来才看旧键。
925
937
  *
938
+ * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改;0.56.0 及更早的
939
+ * 内部名是 `readOfferSupply`,**纯改名**没有第二个消费点)。宿主手里拿到的是**一整帧/一整行**,
940
+ * 上面同时可能有 `ruleOffers`(新)与 `ruleSuggestions`(旧)两个键 —— 这一口是三端唯一该调的那个:
941
+ * `readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions)`。
942
+ * 两代键的取舍序是**判据不是便利**(见下面两段红条),端各写一遍必然在 `null` 那一格上各错一遍。
943
+ *
926
944
  * 🔴 **新键在场即定局,绝不混编**:新键**有载体**而读出空(数组在但全条坏形、或压根不是数组)
927
945
  * 也**不**回落旧键 —— 一台 7.46 引擎不会同时按两代形铸候选,拿旧键顶上去等于把一份异源素材
928
946
  * 冒充成这次 ask 的候选。
@@ -938,7 +956,7 @@ function readLegacyRuleSuggestions(v) {
938
956
  * ⇒ 取「null == 缺席」。安全面上它**不新增**任何攻击面:一个能塞 `{ruleOffers:null, ruleSuggestions:[…]}`
939
957
  * 的注入面,同样能只塞 `{ruleSuggestions:[…]}`,而后者为了兼容 7.44 本来就必须收。
940
958
  */
941
- function readOfferSupply(offers, legacy) {
959
+ export function readRuleOfferSupply(offers, legacy) {
942
960
  // 🔴 `null` 与 `undefined` 同视为缺席(理由见顶注的代价不对称段);其余一切载体 = 新键在场,
943
961
  // 读出什么就是什么,**绝不**再看旧键。
944
962
  if (offers !== undefined && offers !== null)
@@ -1010,7 +1028,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1010
1028
  // 显式 `false`,而缺席的语义是「没有治理来源的证据」,不是「这门可以被表态掀掉」)。
1011
1029
  // #334(0.43.0):新键 `ruleOffers`(server ≥7.46.0 判别联合)优先,旧键 `ruleSuggestions`
1012
1030
  // (≤7.45)归一成 `kind:'single'` 兜底 —— 两代经同一把窄读器,包内出口只有一个形。
1013
- const ruleOffers = readOfferSupply(frame.ruleOffers, frame.ruleSuggestions);
1031
+ const ruleOffers = readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions);
1014
1032
  const card = await surfaceApprovalCard({
1015
1033
  toolName,
1016
1034
  args: args,
@@ -1055,7 +1073,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1055
1073
  ? { delegation: frame.delegation }
1056
1074
  : {}),
1057
1075
  // #225 件1 / #334 换形(0.43.0):规则候选透传(合形项;缺席/坏形 ⇒ 键不 stamp,卡形不渲该档)。
1058
- // 落位是**包内单一形出口** `ruleOffers`,两代 wire 键同经 readOfferSupply 归一。
1076
+ // 落位是**包内单一形出口** `ruleOffers`,两代 wire 键同经 readRuleOfferSupply 归一。
1059
1077
  ...(ruleOffers !== undefined ? { ruleOffers } : {}),
1060
1078
  // #144:被越级的持久规则原文(UNTRUSTED-for-display)。窄化=**非空白串才 stamp**,坏形降缺席
1061
1079
  // (server 明说空串不铸键:「空串是坏值不是『空规则』」)—— 一格空白的规则解释比没有解释更坏。
package/dist/seam.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
10
10
  import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
11
+ import type { WiringManifestAutoMode, WiringManifestModelGate } from './adapter/downstream/eventToSdkMessage.js';
11
12
  /**
12
13
  * `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
13
14
  *
@@ -389,7 +390,36 @@ export type ChromeEvent = {
389
390
  * `actor.hostAsserted` 是消费端唯一能判「这个署名可信吗」的位:渲署名而不渲这个位 = 把一个
390
391
  * 未经验证的名字渲成可信的(core 自己的 `[from …]` 渲染就是靠它决定加不加 `(unverified)`)。
391
392
  */
392
- | HumanInputChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent;
393
+ | HumanInputChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | WiringManifestChromeEvent;
394
+ /**
395
+ * {@link ChromeEvent} 的 `wiring_manifest` 臂(core #524 + core 147③,server ≥7.58.0)——
396
+ * 引擎接线自述里**两段面向终端用户的事实**,其余每一段仍不投影(射程见 eventToSdkMessage 的
397
+ * `case 'wiring_manifest'` 头注)。
398
+ *
399
+ * ── 🔴 宿主消费义务(三条,全部是「不许做什么」)────────────────────────────────────────────
400
+ * ① **缺席不可反推**。`modelGate` 缺席 = 本 run 没有门卸(core 只在真卸时铸段),
401
+ * `autoMode` 缺席 = 老 mint / 外部 derive **没报**。两者都**不许**被渲成一句肯定句
402
+ * (「没有工具被卸掉」/「auto 未武装」)—— 那是把「没报」说成「报了个否」。
403
+ * 本臂在两段都不成形时**根本不会到达**,所以宿主见到本臂就至少有一段是真读数。
404
+ * ② **`autoMode.reason` 六词逐字呈现,不许映射**到 `/v1/capabilities.permissionModeAuto.reason`
405
+ * 的六词:两套词表**同名不同义**(`settings_denied` 在 capabilities 那边折 `no_intent`
406
+ * 不折 `denied`)。要两面都说,就两面各自读、各自渲,绝不归一。
407
+ * ③ **`modelGate.restore` 原样呈现**:它是 core 铸的**逐字**恢复办法,宿主自己拼一句
408
+ * 「试试把某某开关关掉」等于替引擎编了一条它没说过的出口。
409
+ * 🔴 **幂等**:durable 腿重放会再送同一帧(与 `workspace_changed`/`engine_notice` 同纪律),
410
+ * 宿主按 run/leg 去重,别按到达次数计数。
411
+ * 缺席(宿主不接本臂)= 这两条披露在该宿主上看不见,**不是**报错。
412
+ */
413
+ export interface WiringManifestChromeEvent {
414
+ kind: 'wiring_manifest';
415
+ laneProof: LaneProof;
416
+ /** 仅本 run 真有门卸时在场(见义务①)。 */
417
+ modelGate?: WiringManifestModelGate;
418
+ /** effective 腿恒在;缺席只表示「没报」(见义务①)。 */
419
+ autoMode?: WiringManifestAutoMode;
420
+ /** core 铸的事件身份(uuidv7 形);wire 未必带 ⇒ 缺席时本键不在场。 */
421
+ eventId?: string;
422
+ }
393
423
  /**
394
424
  * {@link ChromeEvent} 的 `text_segment_end` 臂(#323 / core #447,core ≥5.63 / server ≥7.50)——
395
425
  * 「**assistant 的这一段散文写完了**」,由引擎明说,不是由本包猜。
package/dist/seam.js CHANGED
@@ -48,6 +48,12 @@ const CHROME_ARM_TABLE = {
48
48
  required: false,
49
49
  duty: '可选:渲引擎通告(按 code+detail,message 仅 fallback;未知 code 也必须渲不许丢;durable 重放按 eventId 幂等;harvest 的 moved/escalated 并列呈现绝不相减)',
50
50
  },
51
+ wiring_manifest: {
52
+ required: false,
53
+ duty: '可选:渲引擎接线自述里的两段用户面事实(modelGate = 本 run 被模型门卸掉的工具 + 逐字恢复办法;' +
54
+ 'autoMode = 武装位 + core 六词原因)。🔴 两段缺席一律不渲肯定句;reason 绝不映射 capabilities 六词;' +
55
+ 'restore 原样呈现;durable 重放按 run/leg 幂等',
56
+ },
51
57
  text_segment_end: {
52
58
  required: false,
53
59
  duty: '可选:引擎明报的 assistant 散文段边界(#323/core #447)。🔴 content 是对账/定界用的权威全文,拿它再渲一行 = 同一段上屏两遍;缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判',
@@ -42,6 +42,17 @@
42
42
  * 这两码在 core 侧早已在铸,是 [4743] #323「completed bg 子代跨重启 SendMessage 复活」把复活裁决
43
43
  * 腿变成常走路径之后才真正会被用户撞见;此前它们双双落进开集兜底,两个不同的下一步被压成一句
44
44
  * 泛泛失败。
45
+ * ⚠️ **0.57.0(L-102)补第九、十格**:`resume.usage_window_exhausted` / `resume.preflight_rejected`
46
+ * 是这一族里**唯一带得出「等多久」**的两码(`retryAfterSec`,server ≥7.47.0 / ≥7.51.0 起在 409 体上
47
+ * additive 携;SDK 8.1.0 起铸 `ResumeRetryLaterError`)。⚠️ **「唯一带得出等待量」≠「唯一可等」**:
48
+ * 同族的 `resume.row_recycling` 也可等(core 铸文逐字「this clears on its own; send again in a
49
+ * moment」= 本文件的 `row-contended` 格),只是 server 给不出秒数 —— 闭集之外不等于「等也没用」。同上一次的机理:两码此前双双落进开集兜底,
50
+ * server 明明给了等待窗,到端只剩一句泛泛失败。判型读口在
51
+ * `wireErrorTriage.resumeRetryLaterFromError`(三端共用,不走本腿的宿主也吃这两码)。
52
+ * 🔴 **两码不是一格而是两格**(异源对抗复审 [medium] 真病修):`resume.preflight_rejected` 有一条
53
+ * `terminal` 臂(token 不可再赎)而判别位只在 message 散文里 ⇒ 仅凭码就归 `retry-later` 会把终局
54
+ * 说成暂时等待。按**正向证据**分:有证据 ⇒ `retry-later`;没有 ⇒ `refused-preflight`(**不可判**,
55
+ * 不是「不可重试」)。证据判据收在读口的 `waitable` 一位上,本层不复判。
45
56
  *
46
57
  * ── UNTRUSTED ───────────────────────────────────────────────────────────────────────────────
47
58
  * 收据 `note` 是 server 铸的文案(引擎会把子代名字拼进去,名字是 spawning model 的自由文本)——
@@ -106,6 +117,39 @@ export type SubagentResumeFailureKind =
106
117
  * 两码合并 = 一半用户白等、另一半白重开。
107
118
  */
108
119
  | 'row-gone'
120
+ /**
121
+ * `resume.usage_window_exhausted` / `resume.preflight_rejected`(L-102,0.57.0;server ≥7.47.0 /
122
+ * ≥7.51.0,SDK 8.1.0 `ResumeRetryLaterError`)—— **有证据表明现在不行、过一会儿行**,也是这一族里
123
+ * **唯一带得出「等多久」**的一格({@link SubagentResumeFailureVerdict.retryAfterSec})。
124
+ * ⚠️ 「唯一带得出等待量」**不是**「唯一可等」——`row-contended` 那一格同样可等(窗口自清),
125
+ * 只是 server 给不出秒数。两件事别混。
126
+ *
127
+ * 🔴 与 `row-contended` **不合并**(两者都是「等」,但不是同一件事,也不是同一个量):
128
+ * `row-contended` 是**行级**的瞬时争用(另一个复活 claim / 一次 reap sweep 正持着它),窗口
129
+ * 自清、server 给不出秒数;本格是**部署级**的时间性拒绝(治理窗满 / 预检拒),server 明确给了
130
+ * 建议等待窗。合并会把一个有确定等待量的格说成「过一会儿再试试」,或者反过来给一个没有窗的
131
+ * 格编一个倒计时。
132
+ * 🔴 与 `retention-lapsed` / `row-gone` / `retain-off` **方向相反**:那三格**等也没用**。
133
+ * 🔴 处置 = **等,不是重发**:resume 是 AT-MOST-ONCE 的有副作用动作,本层照旧一格都不重试 ——
134
+ * 「可以再试」是说给**人**听的,不是给自动重试腿的授权。
135
+ * 🔴 **本格只收有正向证据的那些**(异源对抗复审 [medium] 真病修):`resume.usage_window_exhausted`
136
+ * 恒进本格(core 铸文的不变量就是证据);`resume.preflight_rejected` 只在 server 真给了等待窗
137
+ * 时进本格,否则落 {@link SubagentResumeFailureKind} 的 `'refused-preflight'`。
138
+ */
139
+ | 'retry-later'
140
+ /**
141
+ * `resume.preflight_rejected` **且 server 没给等待窗**(L-102,0.57.0)—— 部署自己的 resume 预检拒了
142
+ * 这次,它是提交前的最后一档,所以**什么都没被消费**;但**还能不能再赎不可判**。
143
+ *
144
+ * 🔴 与 `retry-later` **禁合并**(异源对抗复审 [medium] 立的格):core 在这一码上有两条臂 ——
145
+ * `retry_later`(行留 pending,同一 token 可再赎)与 `terminal`(行已被单发 expire CAS 结清,
146
+ * token 不可再赎)——而**判别位在 message 散文里**,wire 上没有机读位。把没有证据的那些一律
147
+ * 渲成「稍后重试」,就是把一个终局说成暂时等待;而本包**不按文案分臂**(按文案分支 = 上游改
148
+ * 一个词就静默空转)。⇒ 诚实的第三条路:**说两条臂都成立的那句**,把「还能不能再赎」交给
149
+ * 引擎那行原文({@link SubagentResumeFailureVerdict.detail})。
150
+ * 🔴 **它不是「不可重试」**:`false` 只是「不可判」。端**不许**渲成终局,也**不许**渲成「稍后重试」。
151
+ */
152
+ | 'refused-preflight'
109
153
  /** 404 —— 未知 run / 非属主(**无存在性谕示**,两者同形)。 */
110
154
  | 'not-found'
111
155
  /** 400 —— 空 content 等入参问题。 */
@@ -115,20 +159,52 @@ export type SubagentResumeFailureKind =
115
159
  export type SubagentResumeOutcome = {
116
160
  ok: true;
117
161
  receipt: string;
118
- } | {
162
+ }
163
+ /** 失败臂 = `ok:false` + {@link SubagentResumeFailureVerdict} 的四位(0.57.0 起含 `retryAfterSec`
164
+ * 与 `code`;两位都只在 L-102 那两格上可能在场。additive,既有按 `reason`/`detail` 读的宿主
165
+ * 一字不用改)。 */
166
+ | {
119
167
  ok: false;
120
168
  reason: SubagentResumeFailureKind;
121
169
  detail: string;
170
+ retryAfterSec?: number;
171
+ code?: string;
122
172
  };
123
173
  /**
124
- * resume 失败 处置分类。机器轴 = `errorCode`(开集);`status` 只用于**码缺席**时的两格粗分
125
- * (404 无存在性谕示 / 400 入参),绝不用数字去猜某个具体 409 成因(六个 409 只有 `errorCode`
126
- * 分得开,按数字分支等于把六种处置压成一种)。
174
+ * {@link classifySubagentResumeFailure} 的判决(命名形;0.57.0 `retryAfterSec` 位达 3 成员抽名 ——
175
+ * 导出签名里不留 ≥3 成员的内联匿名形,typeshape B4 口径。结构与 0.56.0 的内联形逐字兼容)。
127
176
  */
128
- export declare function classifySubagentResumeFailure(e: unknown): {
177
+ export interface SubagentResumeFailureVerdict {
129
178
  reason: SubagentResumeFailureKind;
179
+ /** server 的原话(UNTRUSTED;呈前由调用方消毒截长)。 */
130
180
  detail: string;
131
- };
181
+ /**
182
+ * **只在** `reason === 'retry-later'` 上可能在场 —— server 给的建议等待秒数(整数 ≥1)。
183
+ * 🔴 **缺席 ≠ 0**:缺席 = 服务端没给窗(老引擎 / 老 SDK 映射 / 该腿不发头),绝不渲一个编出来的
184
+ * 倒计时;别的 `reason` 上本位恒缺席。窄读域见 `resumeRetryLaterFromError`(同包
185
+ * `wireErrorTriage.ts`)。
186
+ * 🔴 **缺席时该说什么由 `reason` 决定,不由本位决定**:`retry-later` 且窗缺席(= 治理窗满而 server
187
+ * 没给数字)⇒ 说「稍后重试」但不给数字;`refused-preflight` ⇒ **不许**说「稍后重试」。
188
+ * 🔴 `reason === 'refused-preflight'` 上本位**恒缺席**——它正是「没有窗」那一格的定义。
189
+ */
190
+ retryAfterSec?: number;
191
+ /**
192
+ * **只在** `reason` 是 `'retry-later'` / `'refused-preflight'` 两格上在场 —— 命中的那一个 wire 码
193
+ * (`resume.usage_window_exhausted` / `resume.preflight_rejected`)。
194
+ *
195
+ * 🔴 **为什么处置分了格还要把码带出来**:`retry-later` 这一格今天有两个来源(治理窗满 / 带窗的
196
+ * 预检拒),而端的诚实措辞不同 —— 前者说「本部署的用量窗满了」,后者说「部署的预检暂时拒了」。
197
+ * `detail` 是 server 的散文,**不是**机读位,别从它反解码。
198
+ * 别的 `reason` 上本位恒缺席(闭集之外本包不认得码,也就不替 server 声明成因)。
199
+ */
200
+ code?: string;
201
+ }
202
+ /**
203
+ * resume 失败 → 处置分类。机器轴 = `errorCode`(开集);`status` 只用于**码缺席**时的两格粗分
204
+ * (404 无存在性谕示 / 400 入参),绝不用数字去猜某个具体 409 成因(那些 409 只有 `errorCode`
205
+ * 分得开,按数字分支等于把好几种处置压成一种)。
206
+ */
207
+ export declare function classifySubagentResumeFailure(e: unknown): SubagentResumeFailureVerdict;
132
208
  /**
133
209
  * 「这一行子代该打到哪条 run」——**纯函数,单点判据**(对抗复审 H1 的修 + 它的可证伪点)。
134
210
  *
@@ -1,4 +1,5 @@
1
1
  import { hostLog } from '../host.js';
2
+ import { resumeRetryLaterFromError } from '../wireErrorTriage.js';
2
3
  import { engineCapTrue } from '../engineCapsCache.js';
3
4
  import { makeEngineWireClient } from '../engineWireSdk.js';
4
5
  import { engineWireTargetFor } from '../engineWireTarget.js';
@@ -51,11 +52,27 @@ function shapeOf(e) {
51
52
  }
52
53
  /**
53
54
  * resume 失败 → 处置分类。机器轴 = `errorCode`(开集);`status` 只用于**码缺席**时的两格粗分
54
- * (404 无存在性谕示 / 400 入参),绝不用数字去猜某个具体 409 成因(六个 409 只有 `errorCode`
55
- * 分得开,按数字分支等于把六种处置压成一种)。
55
+ * (404 无存在性谕示 / 400 入参),绝不用数字去猜某个具体 409 成因(那些 409 只有 `errorCode`
56
+ * 分得开,按数字分支等于把好几种处置压成一种)。
56
57
  */
57
58
  export function classifySubagentResumeFailure(e) {
58
59
  const { status, errorCode, message } = shapeOf(e);
60
+ // L-102(0.57.0):时间性拒绝二码 —— **先判具名闭集**(与 SDK `classifyApiError` 同序:具名码排在
61
+ // `resume.` 前缀兜底之前)。此前这两码双双落进末尾的开集兜底 `error`,于是 server 明明给了
62
+ // 「等多久」,到端只剩一句泛泛失败(与 #318 件④ 收 `row_recycling`/`row_gone` 那次同形)。
63
+ // 读口在 `wireErrorTriage`,不在这里就地重判:同一把窄读器还要服务不走本腿的宿主(decide/park
64
+ // 腿也吃这两码),两处各写一遍就是两个会各自漂的判官。
65
+ const retryLater = resumeRetryLaterFromError(e);
66
+ if (retryLater !== null) {
67
+ // 🔴 **按证据分两格,不按码分**:`waitable` 是读口那一位(全包单一判断点)——`terminal` 臂的
68
+ // 预检拒没有可等证据,归 `refused-preflight`(不可判),绝不渲成「稍后重试」。
69
+ return {
70
+ reason: retryLater.waitable ? 'retry-later' : 'refused-preflight',
71
+ detail: message,
72
+ code: retryLater.code,
73
+ ...(retryLater.retryAfterSec !== undefined ? { retryAfterSec: retryLater.retryAfterSec } : {}),
74
+ };
75
+ }
59
76
  if (errorCode === 'resume.retain_off')
60
77
  return { reason: 'retain-off', detail: message };
61
78
  if (errorCode !== undefined && RETENTION_LAPSED_CODES.has(errorCode)) {
@@ -115,3 +115,76 @@ export interface ScenarioDenyDetail {
115
115
  * 🔴 键位([2055] 死键纪律):只认 `errorCode`,退役 `code` 键不做兼容(clean-cut)。
116
116
  */
117
117
  export declare function scenarioDenyFromError(err: unknown): ScenarioDenyDetail | null;
118
+ /**
119
+ * {@link resumeRetryLaterFromError} 的结构化读数。
120
+ *
121
+ * 判定层**不依赖 SDK 类型面**(与 {@link ScenarioDenyDetail} 同款):注入面可能是宿主的裸 fetch、
122
+ * desktop 的 IPC 转投、web 跨 bundle 的 plain object,也可能是比本包新一版的 SDK ——
123
+ * 这些形上 `instanceof ResumeRetryLaterError` 一律为假,而 `errorCode` 恒在。
124
+ */
125
+ export interface ResumeRetryLaterDetail {
126
+ /**
127
+ * 命中的那一个码({@link RESUME_RETRY_LATER_CODES} 的成员之一)。
128
+ * 🔴 **两码不是同一个处置**:能不能等看 {@link waitable}(本码 ∧ 有没有窗),本位只回答「是哪一个
129
+ * 成因」—— 治理窗满(与部署的账本配额有关)还是部署预检拒(与部署自己那只 `resumePreflight`
130
+ * 有关)。排障要分得清,措辞也要分得清;**别拿本位反推可等性**。
131
+ */
132
+ code: string;
133
+ /**
134
+ * server 给的**等待秒数**。
135
+ * 🔴 **缺席 = 服务端没给窗**,不是 0、不是「立刻」—— 绝不渲一个编出来的倒计时
136
+ * ([honest-absence-not-fabricated-zero])。≤7.46 引擎、以及**把这条错误经 SDK ≤8.0 映射
137
+ * 过来**的宿主(那些版本上本码落无字段的族基类 `SubagentResumeConflictError`)都恒缺席。
138
+ * 🔴 **「缺席时该说什么」不由本位决定,由 {@link waitable} 决定**:`waitable === true` 而窗缺席
139
+ * (治理窗满、或老 SDK 把窗吞了)⇒ 说「稍后重试」但不给数字;`waitable === false` ⇒ **不许**
140
+ * 说「稍后重试」(见那一位顶注:那是不可判,不是可等)。
141
+ */
142
+ retryAfterSec?: number;
143
+ /**
144
+ * **等一会儿到底有没有用** —— 本口唯一的处置位,也是全包对这个问题的**单一判断点**。
145
+ *
146
+ * 🔴 **为什么不是「命中本族即可等」**(异源对抗复审 [medium] 真病修):`resume.preflight_rejected`
147
+ * 在 core 侧有**两条臂** —— 缺省的 `retry_later`(行留 pending,同一 token 障碍清除后仍可赎)
148
+ * 与显式 `terminal`(行已被这次拒绝的单发 expire CAS 结清,token **不可再赎**)——而**判别位
149
+ * 在 message 散文里**,wire 上没有机读位。仅凭码就宣告「稍后重试」,会把一个终局说成暂时等待。
150
+ * 本包**不按文案分臂**(按文案分支正是上游改一个词就静默空转的形),所以只认**正向证据**:
151
+ * · `resume.usage_window_exhausted` ⇒ 恒 `true` —— core 铸文的不变量是「什么都没消费、
152
+ * 什么都没解钉,同一 token 带同一决议在窗放开后可直兑」,这一码本身就是证据;
153
+ * · `resume.preflight_rejected` ⇒ **只有** server 给了等待窗({@link retryAfterSec} 在场)才 `true`;
154
+ * 窗缺席时是 `false` = **不可判**,不是「不可重试」。
155
+ * 🔴 **「有窗 ⇒ 一定是 retry_later 臂」是直证不是推断**(core 7.3.1 fixture 直读
156
+ * `@sema-agent/core/dist/core/runner/runtask.js` 的 `resumePreflight` 拒绝段):
157
+ * · `terminal` 臂(`disposition === 'terminal'` ∧ 有可读 message ⇒ 单发 `store.expire` CAS 结清、
158
+ * 文末逐字 "the token is not redeemable")抛的 `CheckpointError` **一个 detail 都不带** ⇒
159
+ * `retryAfterMs` 结构上不存在 ⇒ server 那一侧无从铸 `retryAfterSec`;
160
+ * · `retry_later` 臂(超时/崩溃/普通拒三支,文末逐字 "the checkpoint stays pending and the same
161
+ * token is redeemable once the obstacle clears")**只在部署真给了 `retryAfterMs` 时**带 detail。
162
+ * ⇒ 窗**在场**是 retry_later 的充分证据;窗**缺席**两臂都可能(retry_later 也常常没有建议),
163
+ * 所以缺席只能读作「不知道」。
164
+ * 🔴 `false` 的正确读法是「**我不知道还能不能再赎**,读引擎那行原文」——**不是**「一定不能」。
165
+ * 两臂都成立的那句话仍可放心说:这一拒发生在提交之前,人的决定没被消费。
166
+ */
167
+ waitable: boolean;
168
+ }
169
+ /**
170
+ * 被 catch 的错误 → resume **时间性拒绝**读数;不是那两个码 ⇒ `null`(绝不误吃这一族里别的 409)。
171
+ *
172
+ * 🔴 **判据只有 `errorCode`,不看 HTTP 数字**:与 `classifySubagentResumeFailure`(同包
173
+ * `subagent/engineSubagentResume.ts`)的既有口径同律(`resume.*` 那一族成员全是 409,按数字分支
174
+ * 等于把几种不同处置压成一种);而本口的
175
+ * 处置是**告诉人等一会儿**——非破坏性,不需要 {@link scenarioDenyFromError} 那种「码 ∧ 状态」的
176
+ * 合取闸(那一条守的是别让一个只是**引用**了该码的响应驱动一次真动作)。
177
+ * 🔴 **结构读不 `instanceof`**:见 {@link ResumeRetryLaterDetail} 顶注(三端注入面 / 跨 bundle 同名类
178
+ * 是两个实例)。键位只认 `errorCode`([2055] 死键纪律,退役 `code` 槽不做兼容)。
179
+ * 🔴 **本口只报事实,处置位是 {@link ResumeRetryLaterDetail.waitable}**:命中本族 **≠** 一定可等 ——
180
+ * `resume.preflight_rejected` 有一条 `terminal` 臂(token 不可再赎)而判别位只在 message 散文里。
181
+ * 「等一会儿有没有用」的判断收在 `waitable` 这一位上(全包单一判断点),名字里的 RetryLater 是
182
+ * 上游 SDK 的类名锚,**不是**本口对每一次命中的断言。
183
+ * 🔴 **`retryAfterSec` 的窄读域 = server 的铸键域**(整数 ∧ ≥1),不更宽也不更窄:
184
+ * · server 铸键逐字是「ms → 秒**向上取整**、**下限 1**」⇒ 真供给里不存在 0 / 负数 / 小数;
185
+ * · 放行 0 就是对消费端说「立刻重试」,而 resume 是 AT-MOST-ONCE 的有副作用动作
186
+ * (叫醒 = 真跑一轮)—— 一个 0 会把「等一会儿」变成热循环;
187
+ * · 放行小数会让端渲出「等 0.4 秒」这种上游从未说过的量。
188
+ * 坏值一律**降缺席**(不是降 0、不是取绝对值):没读到窗与读到一个假窗,前者诚实。
189
+ */
190
+ export declare function resumeRetryLaterFromError(err: unknown): ResumeRetryLaterDetail | null;
@@ -24,7 +24,7 @@
24
24
  * `failed to fetch`/`networkerror`/`network error`/`load failed`,那是宿主词,由 web 在自己那半场
25
25
  * 叠加)。web 的 run-error-table-parity-test(逐 token 读壳源码)随本件退役为「共用同一 import」。
26
26
  */
27
- import { DRAINING_ERROR_CODE, RESUME_AT_ERROR_CODE_PREFIX, SCENARIO_NOT_ALLOWED_ERROR_CODE, } from './engineErrorCodes.js';
27
+ import { DRAINING_ERROR_CODE, RESUME_AT_ERROR_CODE_PREFIX, RESUME_RETRY_LATER_CODES, RESUME_USAGE_WINDOW_EXHAUSTED, SCENARIO_NOT_ALLOWED_ERROR_CODE, } from './engineErrorCodes.js';
28
28
  /**
29
29
  * 网络/传输层失败的词面基表(壳 seamQuery「件2c transport 收窄」的那条正则逐字)。
30
30
  * 判据变更义务:加词=各端跟批;删词/改形=先与消费端对表(web 叠加宿主词的半场见其
@@ -215,3 +215,42 @@ export function scenarioDenyFromError(err) {
215
215
  : [];
216
216
  return { allowlist };
217
217
  }
218
+ /**
219
+ * 被 catch 的错误 → resume **时间性拒绝**读数;不是那两个码 ⇒ `null`(绝不误吃这一族里别的 409)。
220
+ *
221
+ * 🔴 **判据只有 `errorCode`,不看 HTTP 数字**:与 `classifySubagentResumeFailure`(同包
222
+ * `subagent/engineSubagentResume.ts`)的既有口径同律(`resume.*` 那一族成员全是 409,按数字分支
223
+ * 等于把几种不同处置压成一种);而本口的
224
+ * 处置是**告诉人等一会儿**——非破坏性,不需要 {@link scenarioDenyFromError} 那种「码 ∧ 状态」的
225
+ * 合取闸(那一条守的是别让一个只是**引用**了该码的响应驱动一次真动作)。
226
+ * 🔴 **结构读不 `instanceof`**:见 {@link ResumeRetryLaterDetail} 顶注(三端注入面 / 跨 bundle 同名类
227
+ * 是两个实例)。键位只认 `errorCode`([2055] 死键纪律,退役 `code` 槽不做兼容)。
228
+ * 🔴 **本口只报事实,处置位是 {@link ResumeRetryLaterDetail.waitable}**:命中本族 **≠** 一定可等 ——
229
+ * `resume.preflight_rejected` 有一条 `terminal` 臂(token 不可再赎)而判别位只在 message 散文里。
230
+ * 「等一会儿有没有用」的判断收在 `waitable` 这一位上(全包单一判断点),名字里的 RetryLater 是
231
+ * 上游 SDK 的类名锚,**不是**本口对每一次命中的断言。
232
+ * 🔴 **`retryAfterSec` 的窄读域 = server 的铸键域**(整数 ∧ ≥1),不更宽也不更窄:
233
+ * · server 铸键逐字是「ms → 秒**向上取整**、**下限 1**」⇒ 真供给里不存在 0 / 负数 / 小数;
234
+ * · 放行 0 就是对消费端说「立刻重试」,而 resume 是 AT-MOST-ONCE 的有副作用动作
235
+ * (叫醒 = 真跑一轮)—— 一个 0 会把「等一会儿」变成热循环;
236
+ * · 放行小数会让端渲出「等 0.4 秒」这种上游从未说过的量。
237
+ * 坏值一律**降缺席**(不是降 0、不是取绝对值):没读到窗与读到一个假窗,前者诚实。
238
+ */
239
+ export function resumeRetryLaterFromError(err) {
240
+ if (typeof err !== 'object' || err === null)
241
+ return null;
242
+ const e = err;
243
+ const code = e.errorCode;
244
+ if (typeof code !== 'string' || !RESUME_RETRY_LATER_CODES.includes(code))
245
+ return null;
246
+ const sec = e.retryAfterSec;
247
+ // 局部名刻意不叫 `window`:本包出浏览器包(portability 门真跑 esbuild --platform=browser),
248
+ // 一个遮蔽宿主全局的同名局部变量在阅读期就是噪音。
249
+ const windowSec = typeof sec === 'number' && Number.isInteger(sec) && sec >= 1 ? sec : undefined;
250
+ return {
251
+ code,
252
+ // 见 {@link ResumeRetryLaterDetail.waitable}:治理窗满这一码本身即证据;预检拒只认「server 真给了窗」。
253
+ waitable: code === RESUME_USAGE_WINDOW_EXHAUSTED || windowSec !== undefined,
254
+ ...(windowSec !== undefined ? { retryAfterSec: windowSec } : {}),
255
+ };
256
+ }