@sema-agent/client-core 0.67.2 → 0.68.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +64 -1
  3. package/dist/adapt/arms.js +27 -2
  4. package/dist/adapt/turnFlags.d.ts +14 -0
  5. package/dist/adapt/turnFlags.js +4 -1
  6. package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
  7. package/dist/adapter/activeRunSelfHeal.js +79 -8
  8. package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
  9. package/dist/adapter/downstream/eventToSdkMessage.js +33 -2
  10. package/dist/adapter/downstream/terminalToSdkResult.d.ts +29 -0
  11. package/dist/adapter/downstream/terminalToSdkResult.js +46 -15
  12. package/dist/adapter/runStream.d.ts +22 -2
  13. package/dist/adapter/runStream.js +139 -38
  14. package/dist/adapter/types.d.ts +4 -1
  15. package/dist/autoModeUnavailable.d.ts +17 -9
  16. package/dist/autoModeUnavailable.js +26 -8
  17. package/dist/classifierStatus.d.ts +32 -4
  18. package/dist/classifierStatus.js +5 -3
  19. package/dist/engineErrorCodes.d.ts +52 -0
  20. package/dist/engineErrorCodes.js +117 -0
  21. package/dist/engineNoticeCodes.d.ts +95 -1
  22. package/dist/engineNoticeCodes.js +124 -1
  23. package/dist/gateVocabulary.d.ts +18 -7
  24. package/dist/gateVocabulary.js +21 -8
  25. package/dist/hitl/parkResolver.d.ts +0 -14
  26. package/dist/hitl/parkResolver.js +22 -9
  27. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  28. package/dist/hitl/toolApprovalWire.js +1 -0
  29. package/dist/ownKey.d.ts +34 -0
  30. package/dist/ownKey.js +36 -0
  31. package/dist/retryStatus.d.ts +13 -2
  32. package/dist/retryStatus.js +4 -1
  33. package/dist/runTerminal.d.ts +87 -14
  34. package/dist/runTerminal.js +89 -15
  35. package/dist/toolResult.js +8 -0
  36. package/dist/workflowClient.d.ts +22 -0
  37. package/dist/workflowClient.js +37 -0
  38. package/docs/INTEGRATION-CLIENTS.md +367 -9
  39. package/package.json +2 -2
@@ -235,7 +235,24 @@ export interface WiringManifestMcpEntry {
235
235
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
236
236
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
237
237
  * standalone usage SDKMessage arm; the run driver folds it into the footer /
238
- * terminal rollup. Returns `undefined` when the optional `usage` is absent.
238
+ * terminal rollup.
239
+ *
240
+ * ── 🔴 0.68.0 BREAKING(core 7.17.0 #711;异源对抗复审轮三实抓的**第五处**)──────────────────
241
+ * 返回 `undefined` 的含义从「`usage` 这一格缺席」收窄成 **「这一轮的账不知道」**,两种入形都答它:
242
+ * ① `ev.usage` 整个缺席 —— #711 之后这**不再是一条合法形**(契约违约;`runStream` 那一层另有
243
+ * 响亮留痕,本读器只如实答「不知道」);
244
+ * ② `usageMissing: true` **且六格全是 0** —— 那正是 core 的**占位**
245
+ * (`rs.turn.turnUsage ?? {六个 0}`,`run-harness-handlers.js:289-293`)。
246
+ * 🔴 为什么必须改:#711 之前,「这一轮没量出账」这件事在 wire 上的形是**不发 usage** ⇒ 本函数答
247
+ * `undefined`;#711 之后同一件事的形变成六个 0,而本函数是**公面导出** —— 不改的话,同一个真实
248
+ * 情形在引擎升级前后由同一个公开读器给出两个相反的答案(「不知道」变成「这一轮恰好花了 0」),
249
+ * 而调用方**一个字都没改**。收窄之后跨引擎升级的答案**逐字不变**。
250
+ * 🔴 **非零照交**:`usageMissing` 并不保证六格是零(core 在同一轮里可能已攒到真数字而另一次调用
251
+ * 报了缺账 ⇒ 判别位与真数字同帧并存)。占位恒为全零,所以**任何一格非零**都说明这一轮真的量到过
252
+ * ⇒ 照交镜像(那些数字是**下界**,而「是不是下界」由帧上的 `usageMissing` / 终帧的
253
+ * `_sema_usage_lower_bound` 回答,不由本函数回答)。
254
+ * ⚠️ 要**原样**的 wire 镜像(不做任何判断)请直接调 {@link turnUsageToModelUsage} —— 那一只是
255
+ * 纯映射,本函数是**带缺席语义的读器**,两者刻意分开。
239
256
  */
240
257
  export declare function turnEndUsage(ev: Extract<AgentEvent, {
241
258
  type: 'turn_end';
@@ -1198,10 +1198,41 @@ function humanInputProjection(ev, ctx) {
1198
1198
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
1199
1199
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
1200
1200
  * standalone usage SDKMessage arm; the run driver folds it into the footer /
1201
- * terminal rollup. Returns `undefined` when the optional `usage` is absent.
1201
+ * terminal rollup.
1202
+ *
1203
+ * ── 🔴 0.68.0 BREAKING(core 7.17.0 #711;异源对抗复审轮三实抓的**第五处**)──────────────────
1204
+ * 返回 `undefined` 的含义从「`usage` 这一格缺席」收窄成 **「这一轮的账不知道」**,两种入形都答它:
1205
+ * ① `ev.usage` 整个缺席 —— #711 之后这**不再是一条合法形**(契约违约;`runStream` 那一层另有
1206
+ * 响亮留痕,本读器只如实答「不知道」);
1207
+ * ② `usageMissing: true` **且六格全是 0** —— 那正是 core 的**占位**
1208
+ * (`rs.turn.turnUsage ?? {六个 0}`,`run-harness-handlers.js:289-293`)。
1209
+ * 🔴 为什么必须改:#711 之前,「这一轮没量出账」这件事在 wire 上的形是**不发 usage** ⇒ 本函数答
1210
+ * `undefined`;#711 之后同一件事的形变成六个 0,而本函数是**公面导出** —— 不改的话,同一个真实
1211
+ * 情形在引擎升级前后由同一个公开读器给出两个相反的答案(「不知道」变成「这一轮恰好花了 0」),
1212
+ * 而调用方**一个字都没改**。收窄之后跨引擎升级的答案**逐字不变**。
1213
+ * 🔴 **非零照交**:`usageMissing` 并不保证六格是零(core 在同一轮里可能已攒到真数字而另一次调用
1214
+ * 报了缺账 ⇒ 判别位与真数字同帧并存)。占位恒为全零,所以**任何一格非零**都说明这一轮真的量到过
1215
+ * ⇒ 照交镜像(那些数字是**下界**,而「是不是下界」由帧上的 `usageMissing` / 终帧的
1216
+ * `_sema_usage_lower_bound` 回答,不由本函数回答)。
1217
+ * ⚠️ 要**原样**的 wire 镜像(不做任何判断)请直接调 {@link turnUsageToModelUsage} —— 那一只是
1218
+ * 纯映射,本函数是**带缺席语义的读器**,两者刻意分开。
1202
1219
  */
1203
1220
  export function turnEndUsage(ev) {
1204
- return ev.usage ? turnUsageToModelUsage(ev.usage) : undefined;
1221
+ // 🔴 **成形判据不是 `!raw`**(异源复审轮四同族):wire 是 JSON、SSE 解析原样透传 ⇒ `usage: null` /
1222
+ // `usage: 7` 这类形真到得了这里。`null` 会让映射在读字段时抛;标量更坏 —— 它会被映射成一份
1223
+ // **看起来已测量**的全零账(`toCcModelUsage(7)` 逐格读不出、逐格折 0)。坏形与缺席同义:
1224
+ // **这一轮的账不知道**。
1225
+ const rawValue = ev.usage;
1226
+ if (typeof rawValue !== 'object' || rawValue === null || Array.isArray(rawValue))
1227
+ return undefined;
1228
+ const raw = rawValue;
1229
+ const unknown = ev.usageMissing === true;
1230
+ // 占位判据 = **六格全零**(不是「有 usageMissing 就算占位」)。逐格按值判,非有限值当 0 看待
1231
+ // (那一格本来就读不出,它不构成「量到过」的证据)。
1232
+ const placeholder = unknown &&
1233
+ [raw.inputTokens, raw.totalInputTokens, raw.outputTokens, raw.cacheReadTokens, raw.cacheWriteTokens, raw.costMicroUsd]
1234
+ .every((v) => !(typeof v === 'number' && Number.isFinite(v) && v !== 0));
1235
+ return placeholder ? undefined : turnUsageToModelUsage(raw);
1205
1236
  }
1206
1237
  /**
1207
1238
  * Defensive open-set read of a `suspended` gate kind for the run driver's HITL
@@ -304,6 +304,35 @@ export interface MutableSubagentUsageRow {
304
304
  */
305
305
  keyFromParentFallback?: true;
306
306
  }
307
+ /**
308
+ * 子代用量面的**下界判别位键名**(`_sema_subagent_usage_partial`)。
309
+ *
310
+ * 🔴 它与终帧的 `_sema_usage_lower_bound` **刻意不同名**(见上段):两者答的是两个问题,
311
+ * 一个消费面同时拿到两者时必须分得出来。
312
+ * 🔴 本包**不在任何 wire 帧上铸它** —— 它是**渲染面**的位(端把一行子代用量交给自己的视图时用)。
313
+ * 包给名与判据,是为了三端零自拼(同 `gateIdentity` 的三条身份键字面同一条纪律)。
314
+ */
315
+ export declare const SEMA_SUBAGENT_USAGE_PARTIAL_KEY = "_sema_subagent_usage_partial";
316
+ /**
317
+ * 一行子代用量的数字**是不是下界**({@link SEMA_SUBAGENT_USAGE_PARTIAL_KEY} 该不该立)。
318
+ *
319
+ * 三个来源**取并**,每一条都能独立让这一行的数字不是最终数:
320
+ * · `row.usageMissing` —— 这只子代**至少有一轮**引擎没报账(数字照累加,但它是下界);
321
+ * · `row.keyCollision` —— 两个命名空间的 id 撞了字面,这一行是**几只子代的账并起来的**;
322
+ * · `opts.tablePartial` —— 整张表对不上引擎的权威合计(`_sema_nested_usage_by_task_partial`)⇒
323
+ * 表里**每一行**都不可证完整。
324
+ *
325
+ * 🔴 **`false` 不是「这一行是最终数」的证据**:它只说「本端没有任何一条理由认为它是下界」。
326
+ * 行本身还没收口(端自己的 store 知道,包不知道)时,端要自己把那一条并进来 —— 所以这只谓词
327
+ * 收一个 `opts`,而不是假装它掌握全部真相。
328
+ * 🔴 非对象 / 缺席入参 ⇒ `false`(答不出,不是断言);坏形位(非 `true` 的值)不当真。
329
+ */
330
+ export declare function subagentUsageIsPartial(row: {
331
+ readonly usageMissing?: unknown;
332
+ readonly keyCollision?: unknown;
333
+ } | null | undefined, opts?: {
334
+ readonly tablePartial?: boolean;
335
+ }): boolean;
307
336
  /** 终帧两个超集键的产物形(见 {@link nestedUsageByTaskParts})。 */
308
337
  export interface SemaNestedUsageByTask {
309
338
  readonly rows: Readonly<Record<string, SemaSubagentUsageRow>>;
@@ -1,4 +1,5 @@
1
1
  import { stamp } from '../types.js';
2
+ import { putOwnKey } from '../../ownKey.js';
2
3
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局读数的**单一读器**(两代字节 → 一个带标因由)。
3
4
  import { isReviewPark, readRunTerminal, runTerminalCode, runTerminalGateToolName, } from '../../runTerminal.js';
4
5
  import { toCcModelUsage } from './turnUsageToModelUsage.js';
@@ -160,22 +161,11 @@ function permissionDenialParts(stats) {
160
161
  }
161
162
  /**
162
163
  * 0.67.1 —— **以 wire 给的 id / 键名当对象键**时的唯一落键姿势(`__proto__` 陷阱)。
163
- *
164
- * 🔴 `Object.prototype.__proto__` 是一个 **accessor**:在一只普通对象上写 `o["__proto__"] = v`
165
- * 走的是那只 setter ——**不产生自有属性**(v 是对象时还顺手改了 `o` 的原型),于是那一行在
166
- * `Object.keys` / `JSON.stringify` 里**整条消失**,连行数都少一。而本文件这几张表的键全都来自
167
- * wire(taskId / modelId / core 开集的 costBreakdown 键名),没有任何一条保证它们不等于这个字面。
168
- * ⇒ 落键一律走 `defineProperty`,与本包 `hitl/crashConverged.ts` 交付快照时的处置**同一条**
169
- * (那里逐字:「落键仍走 `defineProperty`(`__proto__` 同理)」)。
170
- *
171
- * 🔴 **不改成 null 原型对象交付**:端拿到的仍是一只正常对象(`hasOwnProperty` / `toString` 都在),
172
- * 本修只改「落键」这一步,不改交付形 —— 换原型会在宿主侧造出一类新的 `TypeError`。
173
- * 描述符与普通赋值**逐位相同**(`writable/enumerable/configurable` 三真),所以除了 `__proto__`
174
- * 这一个字面,其余每一个键的行为一个字节都没变。
164
+ * 🔴 0.68.0 / L-246 B2:实现**下沉到 `src/ownKey.ts`**(单源)—— 修前本文件与
165
+ * `hitl/parkResolver.ts` 各持一份同形实现,理由、陷阱与「不改成 null 原型交付」的取舍都在
166
+ * 那个模块的头注里。本别名保留是为了本文件三十余处调用点零改动。
175
167
  */
176
- function putOwn(table, key, value) {
177
- Object.defineProperty(table, key, { value, enumerable: true, writable: true, configurable: true });
178
- }
168
+ const putOwn = putOwnKey;
179
169
  /** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是事实不是缺席)。 */
180
170
  function finiteOrAbsent(v) {
181
171
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
@@ -319,6 +309,47 @@ function costFactParts(stats, observed) {
319
309
  // (本位在上面与流内观测取并后已铸;这里不再重复。)
320
310
  };
321
311
  }
312
+ // ══ 0.68.0(L-244 包侧半场)—— 子代用量「这笔账是下界」的**自有位**,与终帧那一位**分名** ══
313
+ //
314
+ // ── 病形(三重复审 A4/B10 在壳上实抓)──────────────────────────────────────────────────────
315
+ // 壳的子代用量 store 把「这一行还没收口(`partial`)」∪「这一轮引擎没报账(`usageMissing`)」两件事
316
+ // 铸成了一个**与终帧同名**的键 `_sema_usage_lower_bound`。两者是**同名异义**:
317
+ // · 终帧那一位答的是「**这条 run 的合计**是下界」(来源 = `stats.usageMissing` ∪ 流内观测);
318
+ // · 子代面那一位答的是「**这一只子代的这一行**现在还不是最终数」(来源 = 行还没收口 / 那一轮没报账)。
319
+ // 同名的代价是消费面分不出自己读到的是哪一个:一个按键名做聚合的面(把所有 `_sema_usage_lower_bound`
320
+ // 收起来渲一句「本次会话的账是下界」)会把一条**只是还没收口的子代行**算成整条 run 的账不可信。
321
+ //
322
+ // ⇒ 包侧给出**自有名**与**唯一判据**,壳/web/desktop 三端照它渲,谁都不再自己拼一个键名:
323
+ /**
324
+ * 子代用量面的**下界判别位键名**(`_sema_subagent_usage_partial`)。
325
+ *
326
+ * 🔴 它与终帧的 `_sema_usage_lower_bound` **刻意不同名**(见上段):两者答的是两个问题,
327
+ * 一个消费面同时拿到两者时必须分得出来。
328
+ * 🔴 本包**不在任何 wire 帧上铸它** —— 它是**渲染面**的位(端把一行子代用量交给自己的视图时用)。
329
+ * 包给名与判据,是为了三端零自拼(同 `gateIdentity` 的三条身份键字面同一条纪律)。
330
+ */
331
+ export const SEMA_SUBAGENT_USAGE_PARTIAL_KEY = '_sema_subagent_usage_partial';
332
+ /**
333
+ * 一行子代用量的数字**是不是下界**({@link SEMA_SUBAGENT_USAGE_PARTIAL_KEY} 该不该立)。
334
+ *
335
+ * 三个来源**取并**,每一条都能独立让这一行的数字不是最终数:
336
+ * · `row.usageMissing` —— 这只子代**至少有一轮**引擎没报账(数字照累加,但它是下界);
337
+ * · `row.keyCollision` —— 两个命名空间的 id 撞了字面,这一行是**几只子代的账并起来的**;
338
+ * · `opts.tablePartial` —— 整张表对不上引擎的权威合计(`_sema_nested_usage_by_task_partial`)⇒
339
+ * 表里**每一行**都不可证完整。
340
+ *
341
+ * 🔴 **`false` 不是「这一行是最终数」的证据**:它只说「本端没有任何一条理由认为它是下界」。
342
+ * 行本身还没收口(端自己的 store 知道,包不知道)时,端要自己把那一条并进来 —— 所以这只谓词
343
+ * 收一个 `opts`,而不是假装它掌握全部真相。
344
+ * 🔴 非对象 / 缺席入参 ⇒ `false`(答不出,不是断言);坏形位(非 `true` 的值)不当真。
345
+ */
346
+ export function subagentUsageIsPartial(row, opts) {
347
+ if (opts?.tablePartial === true)
348
+ return true;
349
+ if (typeof row !== 'object' || row === null)
350
+ return false;
351
+ return row.usageMissing === true || row.keyCollision === true;
352
+ }
322
353
  /**
323
354
  * L-228 —— 流内子代分表 → 终帧两个超集键的**唯一 mint 点**(成功臂与错误信封共用)。
324
355
  *
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { AgentEvent } from '@sema-agent/sdk';
34
34
  import { type SDKMessage, type EmitContext, type ModelUsage } from './types.js';
35
- import type { EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
35
+ import { type EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
36
36
  /**
37
37
  * 409 body 的 pendingGate 材料(A-028.1,2026-08-12 具名化并补 `governanceForced` 位 ——
38
38
  * 此前包侧只有 {kind, decidePath} 两位,壳侧抄件已多出该位 = 同一 wire 位两份解析器形不同,
@@ -178,14 +178,34 @@ export declare function _resetDroppedFrameReportForTest(): void;
178
178
  * 证不了「表有没有涨」;两者在到顶之后恰好分道扬镳,所以必须直接读表)。 */
179
179
  export declare function _droppedFrameMemoSizeForTest(): number;
180
180
  export interface RunStreamHandle {
181
- /** The latest folded turn usage (footer counters); updated on each turn_end. */
181
+ /**
182
+ * The latest folded turn usage (footer counters).
183
+ *
184
+ * 🔴 **0.68.0 收窄成「最近一次**已测量**的 turn」**(core 7.17.0 #711 的跟车修;异源对抗复审实抓):
185
+ * #711 之后,一轮没量出账时引擎发的是**六个 0 + `usageMissing:true`**(不再是「不发 usage」)。
186
+ * 修前这里是无条件覆盖 ⇒ 那种轮会把一份**占位全零**盖进来,而本形上**没有任何判别位** ——
187
+ * 只吃这个出口的 footer 于是把「不知道」渲成一笔精确的零账(消息臂与 chrome 臂上的判别位
188
+ * 保护不到这个出口)。⇒ 未测量的那一轮**不覆盖**(保留上一次真读数,与 #711 之前的行为逐字
189
+ * 相同),并由 {@link latestUsageMissing} 说出「最新那一轮没测出账」。
190
+ */
182
191
  latestUsage?: ModelUsage;
183
192
  /**
184
193
  * [2295] 裁 ② 逐字通道:与 latestUsage 同拍更新的引擎 `turn_end.usage` **原形**(六键含
185
194
  * `totalInputTokens`)。镜像键求和≠总量(仅 cache 族一致时相等),总量消费面吃这份。
186
195
  * 旧引擎(core <3.0.0)wire 缺形时为 undefined —— 诚实缺席,不造零值。
196
+ * 🔴 0.68.0:与 {@link latestUsage} **同拍同律** —— 未测量的轮不覆盖。
187
197
  */
188
198
  latestEngineUsage?: EngineTurnUsage | undefined;
199
+ /**
200
+ * 🔴 0.68.0 —— **最新那一轮的账知不知道**。在场(恒 `true`)= 最近走过的那个 `turn_end`
201
+ * **没测出账**(`usageMissing:true`),或者它是一条**契约违约**帧(连 usage 都没有);
202
+ * 此时上面两份读数属于**更早的**那一轮,别当成最新那一轮的账。
203
+ *
204
+ * 🔴 **never false**:测量到账的那一轮**把这一位删掉**(键不在场 ⇔ 上面两份就是最新那一轮的账)。
205
+ * 它是**逐轮**的判别位,不是「这条流上曾经有过缺口」的单调位 —— 后者在终帧上
206
+ * (`_sema_usage_lower_bound`),两者答的是两个问题。
207
+ */
208
+ latestUsageMissing?: true;
189
209
  }
190
210
  export declare function isRunStreamActive(): boolean;
191
211
  export declare function runStream(events: AsyncIterable<AgentEvent>, ctx: EmitContext, handle?: RunStreamHandle): AsyncGenerator<SDKMessage>;
@@ -1,5 +1,6 @@
1
1
  import { eventSeq, } from './types.js';
2
2
  import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
3
+ import { turnUsageToModelUsage } from './downstream/turnUsageToModelUsage.js';
3
4
  // D-3(0.66.0):终局对账臂与终帧超集键共用**同一个**成本读器(readRunCostFacts)。
4
5
  import { readRunCostFacts, terminalToSdkResult } from './downstream/terminalToSdkResult.js';
5
6
  import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
@@ -244,6 +245,26 @@ function sanitizeFrameType(type) {
244
245
  * 两处 fail-soft 同款);而②的让位前提是「sink 真接住了」,没接住就得把 console 那腿还回来
245
246
  * —— 否则一个坏 sink 会让丢帧比装它之前更隐蔽(装了个东西反而更瞎,是最坏的一种)。
246
247
  */
248
+ /**
249
+ * 判词 → 这一行说给人听的那句话。**开集**(`why` 是投影器给的开集判词)⇒ 表外判词走缺省句,
250
+ * 绝不因为多了一个判词就不说话。
251
+ *
252
+ * 🔴 为什么要分句而不是一句通用的:缺省那句逐字说「this build's projector has no arm for it」——
253
+ * 对 `unknown_arm` / `malformed` 是真话,对 **0.68.0 的契约违约判词是假话**(本 build 有臂,
254
+ * 是上游那一帧违了自己声明的契约)。一句说错方向的诊断会把读它的人指去升级客户端,而该做的是
255
+ * 去看引擎那一侧。
256
+ */
257
+ const DROPPED_WHY_SENTENCE = Object.freeze({
258
+ turn_end_usage_absent: "the engine declares `turn_end.usage` as always present (core >= 7.17.0), and this frame has none. " +
259
+ 'The turn is counted as UNKNOWN spend (the run total is reported as a lower bound), and the frame itself renders NOWHERE.',
260
+ });
261
+ const DROPPED_WHY_SENTENCE_DEFAULT = "this build's projector has no arm for it (engine newer than the client, or a malformed frame). It renders NOWHERE.";
262
+ /** 🔴 **按自有属性查表**(本仓对措辞表的既定纪律):`Object.freeze` 不移除原型,裸下标会让一个
263
+ * 叫 `constructor` / `toString` 的判词命中 `Object.prototype` 上的**函数**并被拼进日志行。 */
264
+ function droppedWhySentence(why) {
265
+ const row = Object.hasOwn(DROPPED_WHY_SENTENCE, why) ? DROPPED_WHY_SENTENCE[why] : undefined;
266
+ return typeof row === 'string' ? row : DROPPED_WHY_SENTENCE_DEFAULT;
267
+ }
247
268
  function reportDroppedFrame(why, type, ctx) {
248
269
  if (ctx.onDroppedFrame) {
249
270
  try {
@@ -267,8 +288,7 @@ function reportDroppedFrame(why, type, ctx) {
267
288
  // eslint-disable-next-line no-console
268
289
  console.error(
269
290
  // ADAPTER-F5 ②:type 是 wire 值,进日志行前必须 sanitize(裸值能伪造额外的整行)。
270
- `[client-core] dropped an engine frame (${why}): type="${sanitizeFrameType(type)}" — this build's projector has no arm ` +
271
- 'for it (engine newer than the client, or a malformed frame). It renders NOWHERE.');
291
+ `[client-core] dropped an engine frame (${why}): type="${sanitizeFrameType(type)}" — ${droppedWhySentence(why)}`);
272
292
  }
273
293
  /** 测试钩:清空「已上报过的臂」去重表(去重是**跨调用**状态,不清就只有第一条用例看得见)。 */
274
294
  export function _resetDroppedFrameReportForTest() {
@@ -409,7 +429,50 @@ async function* runStreamInner(events, ctx, handle = {}) {
409
429
  usageMissingObserved = true;
410
430
  const stopReasonRaw = ev.stopReason;
411
431
  const stopWord = typeof stopReasonRaw === 'string' && stopReasonRaw.length > 0 ? stopReasonRaw : undefined;
412
- const usage = turnEndUsage(ev);
432
+ // 🔴 `engineUsage` = 引擎**逐字原形**([2295] 裁 ②);`usage` = 它的 CC 镜像
433
+ // (`turnUsageToModelUsage` 是唯一铸口,footer 折叠与本臂用的是**同一只产物**,不另铸第二份)。
434
+ // 两者同拍取、同拍过下面那道违约闸 —— 闸后**都恒在场**,下游一处条件判都不再需要。
435
+ // 🔴 0.68.0:除它们之外还要**第三只读数**,因为本层要答的是**两个不同的问题**:
436
+ // · `engineUsage` —— 「这一帧有没有 usage 这个格子、而且它**成形**」(违约闸的判据);
437
+ // · `measured` —— 公面读器 `turnEndUsage` 的答案:`undefined` = **这一轮的账不知道**
438
+ // (整格缺席 / 占位六零)。它与 `usage`(逐字镜像)刻意分开 —— 把两件事合成一个读数,
439
+ // 一条**合法**的占位帧就会在违约闸上被误判成违约并整帧丢掉。
440
+ // 🔴 **成形判据不是 `!== undefined`**(异源对抗复审轮四实抓):wire 是 JSON,SSE 解析**原样透传**
441
+ // ⇒ `usage: null` / `usage: 7` 这类形真到得了这里。`null` 会让下面的映射在读 `costMicroUsd`
442
+ // 时抛 `TypeError` —— 那不是「一帧读不懂」,那是**整条流当场断掉**(后面的 `done` 一并丢);
443
+ // 标量则更坏:它会被映射成一份**看起来已测量**的全零账。⇒ 一律先判「非 null 的非数组对象」,
444
+ // 坏形与缺席走**同一条**违约路(响亮 + 不投影 + 立下界位)。
445
+ const rawUsage = ev.usage;
446
+ const engineUsage = typeof rawUsage === 'object' && rawUsage !== null && !Array.isArray(rawUsage)
447
+ ? rawUsage
448
+ : undefined;
449
+ const usage = engineUsage !== undefined ? turnUsageToModelUsage(engineUsage) : undefined;
450
+ // 「这一轮的账知不知道」的**唯一判据**(与公面同一只;本层不另写一份「六格全零」的判断)。
451
+ const measured = turnEndUsage(ev);
452
+ // ── 🔴 0.68.0 BREAKING(core 7.17.0 #711)—— `turn_end.usage` **恒在场** ────────────────
453
+ // core 的铸点自 7.17.0 起是无条件的:`const usage = rs.turn.turnUsage ?? {六个 0}` 后
454
+ // `queue.push({type:"turn_end", usage, ...(turnUsageUnknown ? {usageMissing:true} : {}), …})`
455
+ // (`run-harness-handlers.js` `onTurnEnd`)⇒ 「没有 usage 的 turn_end」**不再是一条合法形**:
456
+ // 那一轮真没量出账时,core 发的是**六个 0 + `usageMissing:true`**,而不是不发 usage。
457
+ // ⇒ 0.65.1 / B-088 那条「usage 缺席也要照发」的臂(以及它逼出来的三处
458
+ // `usage !== undefined ? … : …` 条件)在本版**整条删掉**:它守的那个输入形已经不存在,
459
+ // 留着它等于给一个契约违约的帧准备一条静默通道。
460
+ //
461
+ // 🔴 **缺席 = 契约违约 ⇒ 响亮**(§32 的「违约无断言」纪律,本批的反钉格):
462
+ // ① 走宿主的丢帧留痕口({@link EmitContext.onDroppedFrame},判词开集 ⇒ 宿主不必改型),
463
+ // 缺 sink 时落 console —— 两条腿都说得出「哪一帧、为什么」;
464
+ // ② **这一轮的账确实不知道** ⇒ 同时立下界位,终帧那些数字按「≥」交付。不立的话,
465
+ // 一条丢了账的 run 会在终帧上被渲成一笔精确的账 —— 正是本仓反复在修的那条病;
466
+ // ③ 整帧**不投影**:没有 usage 就没有任何数字可交,折 0 就是把「不知道」写成已知账。
467
+ // 丢掉的只有同帧可能带的 `stopReason` —— 一条违约帧上的附带位不值得为它保留一条
468
+ // 「半读」臂(那条臂就是 ① 要消灭的静默通道)。
469
+ if (engineUsage === undefined || usage === undefined) {
470
+ usageMissingObserved = true;
471
+ // 🔴 违约帧同样让 footer 出口说实话:这一轮的账不知道,上面那两份读数属于更早的一轮。
472
+ handle.latestUsageMissing = true;
473
+ reportDroppedFrame('turn_end_usage_absent', ev.type, ctx);
474
+ continue;
475
+ }
413
476
  // §E2 identity (service 1.78) — a SUB-FLOW's turn_end (orchestration/subagent round, carries
414
477
  // the identity envelope) must NOT drive the leader's C1a `end` reconcile: its outputTokens are
415
478
  // the child's, and reconciling the leader's responseLength against them is the token-jump bug
@@ -431,10 +494,28 @@ async function* runStreamInner(events, ctx, handle = {}) {
431
494
  // `parent` / `sourceTaskId`),两层问的不是同一个问题:信封在不在 vs 这一行归到谁名下。
432
495
  const isSubFlow = ev.parentToolCallId !== undefined ||
433
496
  ev.sourceTaskId !== undefined;
434
- if (usage) {
435
- handle.latestUsage = usage;
436
- // [2295] 裁 ② 逐字通道:与镜像同拍存一份引擎原形(六键含 totalInputTokens)。
437
- handle.latestEngineUsage = ev.usage;
497
+ // 🔴 0.68.0:此处修前是 `if (usage) {` —— 那条 `usage` 在不在的判据随 core 7.17.0 的
498
+ // 「恒在场」一起退役(缺席在上面的违约闸里已经整帧收口)。块保留是为了**不动缩进**,
499
+ // 读法上它已经是无条件的一段。
500
+ {
501
+ // 🔴 0.68.0(异源对抗复审 [medium] 实抓,轮四再订正一次)—— 这个公开出口上**没有**判别位
502
+ // 可以让 footer 分辨「占位」与「真零」,所以它要分两件事各自决定:
503
+ // ① **读数**:只有**占位**(`measured === undefined` 且这一帧成形)才不覆盖 —— 保留上一次
504
+ // 真读数,与 #711 之前的行为逐字相同(那时这种轮根本不带 usage)⇒ 没跟车的消费者零回归。
505
+ // 🔴 **缺账 ≠ 占位**:`usageMissing` 可以与**真数字同帧**(core 在同一轮里攒到过数字而
506
+ // 另一次调用报了缺账)—— 那些数字是真的量到过(是下界),blanket 跳过会把它们丢掉
507
+ // (轮四实测:5 → 42+缺账位,main 的 handle 到 42,blanket 写法停在 5)。
508
+ // ② **判别位**:只要这一轮报了缺账就立(never false;测到账的那一轮删键)。
509
+ // 两件事分开之后,「读数是最新的真值」与「最新那一轮的账不全」可以同时为真。
510
+ if (measured !== undefined) {
511
+ handle.latestUsage = usage;
512
+ // [2295] 裁 ② 逐字通道:与镜像同拍存一份引擎原形(六键含 totalInputTokens)。
513
+ handle.latestEngineUsage = engineUsage;
514
+ }
515
+ if (usageMissing)
516
+ handle.latestUsageMissing = true;
517
+ else
518
+ delete handle.latestUsageMissing;
438
519
  // plugins 专项 G1(2026-07-21):同一折叠点多发一份给 lastTurnUsageStore——StatusLine
439
520
  // 的 statusline 命令 stdin(context_window.current_usage)在消息面无 usage(seam 合成
440
521
  // 消息不带)时回落到这里,claude-hud 类插件的 Context 条才有真值。sub-flow 的 turn_end
@@ -452,7 +533,9 @@ async function* runStreamInner(events, ctx, handle = {}) {
452
533
  kind: 'last_turn_usage',
453
534
  laneProof: MAIN,
454
535
  usage,
455
- ...(ev.usage !== undefined ? { engineUsage: ev.usage } : {}),
536
+ // 🔴 0.68.0:`engineUsage` 的条件 spread 退役 —— `turn_end.usage` 恒在场
537
+ // (缺席已在违约闸里整帧收口),这里再判一次就是给一个不可能的形留座位。
538
+ engineUsage,
456
539
  // L-215③:chrome 腿同批带这两位(message 腿的对偶在 `turn_usage` 臂的 `_sema_` 键上)。
457
540
  // 🔴 `usageMissing` 在这条腿上**不能**靠「不发 usage」表达 —— 本臂的 `usage` 是必填位
458
541
  // (宿主义务是「落最近一次 turn 真 usage」),所以它只能以判别位在场:
@@ -506,10 +589,10 @@ async function* runStreamInner(events, ctx, handle = {}) {
506
589
  // 行键读不出(两键都缺 / 都是空串)仍然整条不入表:编一个 `"unknown"` 行会把几只子代
507
590
  // 的账混成一只(C3 那一格守的就是这条)。
508
591
  if (taskId !== undefined) {
509
- // 🔴 发臂条件与主臂 0.65.1 / B-088 **逐字同族**:core 真会发**裸**
510
- // `{type:'turn_end', usageMissing:true}`(无 usage、无 stopReason),旧条件「有 usage 才发」
511
- // 会让**最诚实的那一帧**整条静默 —— 那是本仓已定谳的病形,子代这条腿不许再犯一次。
512
- // 三者任一在场即发;三者皆缺席仍不发。
592
+ // 🔴 0.68.0 BREAKING:发臂条件的「三者任一在场」整条退役 —— 它是 0.65.1 / B-088 为
593
+ // 「裸 `{type:'turn_end', usageMissing:true}`(无 usage)」那一形写的,而 core 7.17.0
594
+ // 起那一形不再存在(`usage` 恒在场,缺席在上面的违约闸里整帧收口)⇒ 走到这里就**恒有
595
+ // 话可说**,条件判只剩车道证明那一条(`parent`)与宿主有没有装 sink。
513
596
  // 🔴 **`parent` 缺席时本臂不发,而行照进表**(0.67.1 定谳,理由如实写在这里):
514
597
  // chrome 信封的车道证明 `LaneProof` 的子流臂是 `{lane:'subagent', parentToolCallId: string}`
515
598
  // (`seam.ts`),座位门 `seatContract.checkLaneProof` 对它是**硬要求**(缺伴随位的信封
@@ -520,15 +603,15 @@ async function* runStreamInner(events, ctx, handle = {}) {
520
603
  // 按 `parentToolCallId: string` 读),不是一个 patch 能做的事。
521
604
  // ⇒ 增量腿在这一形上静默,**收口快照(终帧 `_sema_nested_usage_by_task`)照带这一行**
522
605
  // —— 账不丢,少的只是这一形的实时增量;两者本来就是「同一份账的两个时刻」。
523
- if (parent !== undefined && (usage !== undefined || usageMissing || stopWord !== undefined) && ctx.emitChrome) {
606
+ if (parent !== undefined && ctx.emitChrome) {
524
607
  emitChromeFireAndForget(ctx, {
525
608
  kind: 'subagent_turn_usage',
526
609
  laneProof: { lane: 'subagent', parentToolCallId: parent },
527
610
  taskId,
528
611
  ...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
529
612
  parentToolCallId: parent,
530
- ...(usage !== undefined ? { usage } : {}),
531
- ...(ev.usage !== undefined ? { engineUsage: ev.usage } : {}),
613
+ usage,
614
+ engineUsage,
532
615
  ...(usageMissing ? { usageMissing: true } : {}),
533
616
  ...(stopWord !== undefined ? { stopReason: stopWord } : {}),
534
617
  });
@@ -547,14 +630,27 @@ async function* runStreamInner(events, ctx, handle = {}) {
547
630
  if (sourceTaskId === undefined)
548
631
  row.keyFromParentFallback = true;
549
632
  row.turns += 1;
550
- row.inputTokens += usage?.inputTokens ?? 0;
551
- row.outputTokens += usage?.outputTokens ?? 0;
552
- // 🔴 `cacheReadTokens` 读的是**引擎原形** `ev.usage`,不是 CC 镜像:镜像的
633
+ // 🔴 0.68.0:两处 `usage?.x ?? 0` 的 `?.`/`?? 0` 退役 —— `usage` 恒在场(违约闸在上面),
634
+ // 留着「缺席折 0」的写法等于在代码里为一个不可能的形保留一条把「不知道」写成 0 的路。
635
+ row.inputTokens += usage.inputTokens;
636
+ row.outputTokens += usage.outputTokens;
637
+ // 🔴 `cacheReadTokens` 读的是**引擎原形** `engineUsage`,不是 CC 镜像:镜像的
553
638
  // `cacheReadInputTokens` 是**必填** number,缺席在那儿已经被折成 0
554
639
  // (`toCcModelUsage` 的 `finiteOrZero`)⇒ 从镜像读就再也分不出「没报」与「零命中」。
555
640
  // ⇒ 一轮都没报过 ⇒ 键**不铸**;报过之后再加 0 的那些轮是真的零命中。
556
- const cacheRead = ev.usage?.cacheReadTokens;
557
- if (typeof cacheRead === 'number' && Number.isFinite(cacheRead)) {
641
+ // 🔴 **0.68.0 跟车修(#711 的同形后果,族扫捞出;异源对抗复审轮一订正过一次)**:
642
+ // `usageMissing` 的那一轮,core 的 `usage` 是 `rs.turn.turnUsage ?? {六个 0}` ——
643
+ // ⚠️ **`usageMissing` 并不保证那六格是零**:core 在同一轮里可能已经攒到过真数字
644
+ // (`turnUsage` 有值)而**另一次**模型调用报了缺账,于是 `turnUsageMissing` 与真数字
645
+ // **同帧并存**(`run-harness-handlers.js:79-101` 与 `:289-293` 真字节)。
646
+ // ⇒ 判据只能锚在**能证明是真读数的那一半**:占位恒为 `0`,所以一个**非零有限数**
647
+ // 必定是真的量到过 ⇒ 照累加;而 `0` 在这一形上**分不出**占位与「零命中」⇒ 不铸
648
+ // (本行的全部意义就是把「没报」与「零命中」分开,那一格会被 #711 悄悄抹平)。
649
+ // 🔴 轮一的写法是「`usageMissing` 的轮整条跳过」,那会把**真的非零 cache 读数丢掉**
650
+ // (两帧 10 / 100 且第二帧带缺账位 ⇒ 修前 110、轮一写法 10,而 chrome 增量腿仍交
651
+ // 10 与 100 ⇒ 实时面与终局分表对不上)。收窄成「只屏蔽零」两面就一致了。
652
+ const cacheRead = engineUsage.cacheReadTokens;
653
+ if (typeof cacheRead === 'number' && Number.isFinite(cacheRead) && (!usageMissing || cacheRead !== 0)) {
558
654
  row.cacheReadTokens = (row.cacheReadTokens ?? 0) + cacheRead;
559
655
  }
560
656
  if (usageMissing)
@@ -562,35 +658,40 @@ async function* runStreamInner(events, ctx, handle = {}) {
562
658
  nestedUsageByTask.set(rowKey, row);
563
659
  }
564
660
  }
565
- const outputTokens = ev.usage?.outputTokens;
566
- // 🔴 异源对抗复审 [medium]③:发臂条件从「有 outputTokens」放宽到「**有话可说**」——
567
- // core 会发 `{type:'turn_end', usageMissing:true, stopReason:'error'}` 这种合法帧,而
568
- // 旧条件让它整条静默 ⇒ 「这一轮为什么停」这条机读位在最需要它的那一刻(出错/中止)不见了。
569
- // ⚠️ 旧消费者零影响:`outputTokens` 读不出时**整键不铸**,而 adapt 的 `turnUsageArm`
570
- // 本来就以 `typeof m.outputTokens === 'number'` 开门 ⇒ 这种帧对它是 no-op。
571
- // 🔴 0.65.1 / B-088(test [6961] 对抗复审轨实抓,core [6962] 证实真铸形 run-harness-handlers.ts:491):
572
- // `usageMissing` 判别位**本身就是话**——core 该轮零 usage 帧时发裸 `{type:'turn_end',
573
- // usageMissing:true}`(无 usage、无 stopReason),旧条件让它整条静默 ⇒ 最诚实的那一帧
574
- // 反而丢了 `_sema_usage_missing`(G30-23)。三者任一在场即发;三者皆缺席仍不发(F4)。
575
- if ((typeof outputTokens === 'number' || stopWord !== undefined || usageMissing) && !isSubFlow) {
661
+ const outputTokens = engineUsage.outputTokens;
662
+ // 🔴 0.68.0 BREAKING:发臂条件的「三者任一在场」整条退役(0.65.1 / B-088 那条判据的
663
+ // 输入形随 core 7.17.0 消失 —— 见本块顶部的违约闸)。走到这里 `usage` 恒在场 ⇒
664
+ // **恒有话可说**:要么是镜像,要么是 `usageMissing` 判别位,两者必有其一。
665
+ // ⚠️ 旧消费者零影响:`outputTokens` 读不出时仍然**整键不铸**,而 adapt 的 `turnUsageArm`
666
+ // 本来就以 `typeof m.outputTokens === 'number'` 开门 ⇒ 那种帧对它照旧是 no-op。
667
+ if (!isSubFlow) {
576
668
  // ── L-215③(0.65.0):assistant 行那两个**算不出来**的键的真值出口 ─────────────────
577
669
  // `eventToSdkMessage` 的 `assistantArm` 刻意**不**在内容臂上铸 `usage` / `stop_reason`
578
670
  // (帧序:内容臂先到、`turn_end` 后到 ⇒ 臂发出时引擎还没说这一轮花了多少;在那里铸只能
579
671
  // 是估算,而估算正是本件要根治的病)。真值只能在**这里**给 —— 这条臂本来就是 turn 收尾
580
672
  // 那一拍的中性出口。两个都是 `_sema_` 超集键,CC 同名键语义零改:
581
673
  // · `_sema_last_assistant_usage` —— 这一轮的 CC `ModelUsage` 镜像(与 footer 折叠用的
582
- // 是**同一只** `turnEndUsage()` 产物,不另铸第二份 ⇒ 两面永远不会各漂各的);
674
+ // 是**同一只** `turnUsageToModelUsage()` 产物,不另铸第二份 ⇒ 两面永远不会各漂各的);
583
675
  // · `_sema_stop_reason` —— `turn_end.stopReason` **原词透传**(core 归一化后的五词
584
676
  // `stop`/`length`/`toolUse`/`error`/`aborted`,sdk 型面是开放 string ⇒ 按开集读;
585
677
  // 「这一轮是不是被 max_tokens 截了」就靠它,此前 stream 与 trace 两面互盲)。
586
- // 缺席一律不铸(旧引擎不发 `stopReason`;`usage` 整体缺席时本臂根本不发,见上面的 if)。
678
+ // 缺席一律不铸(旧引擎不发 `stopReason`;`usage` 整体缺席的帧在违约闸那一拍就收口了)。
587
679
  yield {
588
680
  type: 'turn_usage',
589
- ...(typeof outputTokens === 'number' ? { outputTokens } : {}),
590
- // 🔴 `usageMissing` 在场 ⇒ **不铸镜像**(铸了就是把「不知道」写成一笔全零的已知账),
591
- // 改铸判别位。两键互斥,消费方一看就知道这一轮的账是不是可信。
592
- ...(usage !== undefined && !usageMissing ? { _sema_last_assistant_usage: usage } : {}),
593
- ...(usageMissing ? { _sema_usage_missing: true } : {}),
681
+ // 🔴 `usageMissing` 在场 ⇒ **不铸镜像**(0.65.x 起的既有规矩:全零的「不知道」绝不冒充
682
+ // 一笔已知的零账),改铸判别位。
683
+ // 🔴 `outputTokens` 的判据随 #711 多一条(与上面子代腿的 `cacheReadTokens` **同一条**):
684
+ // 缺账轮的 `usage` 是 `turnUsage ?? {六个 0}`,而 `usageMissing` **不保证**那六格是零
685
+ // (同一轮里另一次调用报了缺账时,真数字与判别位同帧并存)⇒ 占位恒为 `0`,所以
686
+ // **非零有限数必定是真读数**(照铸),而 `0` 在这一形上分不出占位与真零 ⇒ 不铸 ——
687
+ // 把占位 `0` 交出去,以 `typeof === 'number'` 开门的既有消费者(adapt 的 `turnUsageArm`
688
+ // → spinner 的 responseLength 对账)会拿它当一次真的「这一轮吐了 0 个 token」。
689
+ ...(usageMissing ? { _sema_usage_missing: true } : { _sema_last_assistant_usage: usage }),
690
+ // `outputTokens` 仍按**值**判:`usage` 恒在场不等于它里面每一格都是有限数,而 wire 是
691
+ // JSON —— 坏值折 0 就是把「读不出」写成一笔零账。读不出 ⇒ 整键不铸。
692
+ ...(typeof outputTokens === 'number' && Number.isFinite(outputTokens) && (!usageMissing || outputTokens !== 0)
693
+ ? { outputTokens }
694
+ : {}),
594
695
  ...(stopWord !== undefined ? { _sema_stop_reason: stopWord } : {}),
595
696
  };
596
697
  }
@@ -47,7 +47,10 @@ export declare function uuid(): string;
47
47
  * 一条被丢弃的引擎帧的**结构化留痕**([C77]①,`EmitContext.onDroppedFrame` 的载荷)。
48
48
  * 两个位都取自 `EventProjection` 的 `dropped` 臂原值,包侧不做任何加工:
49
49
  * · `type` = 引擎那一帧的 `type`(未知臂/畸形帧的臂名);
50
- * · `why` = 投影器给的判词(今天是 `unknown_arm` / `malformed`)。
50
+ * · `why` = 投影器给的判词(今天是 `unknown_arm` / `malformed` / `turn_end_usage_absent`)。
51
+ * 🔴 第三个词是 **0.68.0 新加**(core 7.17.0 #711 之后 `turn_end.usage` 恒在场 ⇒ 缺席是
52
+ * **契约违约**而不是一条合法形):它是本包第一处「不是渲不出来,而是上游违约」的判词,
53
+ * 正因为 `why` 是开集,宿主不必改型就能收到它。
51
54
  * 🔴 **不是**开集枚举:`why` 故意留成 string —— 投影器长出新判词时宿主不该编译不过,
52
55
  * 它本来就是「说给人看的一句判词」,不是控制流上的判别位。
53
56
  */
@@ -61,7 +61,9 @@
61
61
  * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
62
62
  * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
63
63
  */
64
- export declare const CLASSIFIER_DENY_CAUSES: readonly string[];
64
+ export declare const CLASSIFIER_DENY_CAUSES: readonly ["unavailable", "parse_error"];
65
+ /** {@link CLASSIFIER_DENY_CAUSES} 的成员型(端的型面改派生,不再手抄两词)。 */
66
+ export type ClassifierDenyCause = (typeof CLASSIFIER_DENY_CAUSES)[number];
65
67
  /**
66
68
  * 这个词是不是 {@link CLASSIFIER_DENY_CAUSES} 的成员(core `isClassifierDenyCause` 的镜像)。
67
69
  *
@@ -71,7 +73,7 @@ export declare const CLASSIFIER_DENY_CAUSES: readonly string[];
71
73
  * 所以在这一格上按闭集判不会「把一个合法的新词吞成缺席」——真读到表外词只说明那条记录本不该长
72
74
  * 这样,而把它渲成一句成因就是替引擎编事实。(`deniedBy` 那张表在 `gateVocabulary.ts` 上同一条。)
73
75
  */
74
- export declare function isClassifierDenyCause(v: unknown): boolean;
76
+ export declare function isClassifierDenyCause(v: unknown): v is ClassifierDenyCause;
75
77
  /**
76
78
  * 一条**门记录**(`tool_end.gate` / `PermissionDeniedPayload.gate` / 耐久行的 resolved outcome,
77
79
  * 或本包 `gateOutcomeOf` 投出的 {@link import('./gateOutcome.js').GateOutcomeView})→
@@ -89,7 +91,7 @@ export declare function isClassifierDenyCause(v: unknown): boolean;
89
91
  * 🔴 **缺席不是断言**:缺席同时覆盖「分类器自己裁决 block 了」「这次不是分类器轮」「本部署没接分类器」
90
92
  * 三形,端**禁**读成「分类器好着呢」。
91
93
  */
92
- export declare function classifierDenyCauseOf(gate: unknown): string | undefined;
94
+ export declare function classifierDenyCauseOf(gate: unknown): ClassifierDenyCause | undefined;
93
95
  /**
94
96
  * 一轮分类**为什么**跑不了(core `AUTO_MODE_UNAVAILABLE_CAUSES`;逐词逐序镜像,core 7.12.0 起两词)。
95
97
  * · `error` —— 模型那条腿抛了/被拒(分类时的路由失败也读在这里:派生路由的前置在任何 decide
@@ -109,10 +111,16 @@ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly string[];
109
111
  *
110
112
  * 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
111
113
  * 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
112
- * 🔴 表外词 / 坏值 ⇒ 一句**兜底**:仍然告诉用户「这次拒是分类器那条腿引出来的」,但**不冒充**
113
- * 两句里的任何一句;原样带上那个词供运维追问上游。
114
- * ⚠️ 与 {@link classifierDenyCauseOf} 的闭集判据**不矛盾**:那一层答「这条事实在不在」(闭集,
115
- * 出集 = 坏记录),本层答「拿到一个词怎么渲」——**渲判据面**的消费端(以及一条从旧持久态恢复
116
- * 回来的视图)可能手里就是一个表外词,它必须渲出一句诚实的话,而不是整屏崩或冒充一句已知的。
114
+ * ── 🔴 0.68.0 / L-245:入参从 `unknown` 收窄成**闭集成员型** ─────────────────────────────────
115
+ * 修前这里留着一句「its reported cause X **is a word newer than this client**」的兜底,而它
116
+ * **结构上走不到**:唯一到达本铸点的路是 {@link classifierDenyCauseOf},那一层已经按闭集把表外词
117
+ * 判成了缺席(理由见该函数:出闭集的 cause 在 core 那边是**记录缺陷**,server 整条不上帧 ⇒ 一个
118
+ * 表外词根本到不了消费端)。一条走不到的兜底有两重坏处:① 它假装这一面是开集,于是没人给这张表
119
+ * 配编译期围栏,core 加词那天这里一声不响;② 它说的那句话是**假的** —— 真有一个「比这一端新」的词
120
+ * 时,它压根不会到这里。
121
+ * ⇒ 入参收窄(表外词现在是**编译期**错误)+ 表型 `Record<ClassifierDenyCause, string>`(加词当天
122
+ * 缺键红)。剩下的运行期兜底只服务一种情形:调用方 cast 绕过型面、或从旧持久态恢复出一个非成员值
123
+ * —— 那时它说的是「这个词不在本端的闭集里」(一句真话),而**不再**冒充「上游比我新」。
124
+ * 🔴 渲染路径**不许抛**(本仓已定谳的病形),所以 never 分支照样交一句话,不 throw。
117
125
  */
118
- export declare function classifierDenyCauseDetail(cause: unknown): string;
126
+ export declare function classifierDenyCauseDetail(cause: ClassifierDenyCause): string;