@sema-agent/client-core 0.67.2 → 0.68.1

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 (50) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/README.md +65 -1
  3. package/dist/adapt/arms.js +67 -9
  4. package/dist/adapt/textStream.d.ts +102 -1
  5. package/dist/adapt/textStream.js +169 -6
  6. package/dist/adapt/turnFlags.d.ts +14 -0
  7. package/dist/adapt/turnFlags.js +4 -1
  8. package/dist/adapt.js +4 -1
  9. package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
  10. package/dist/adapter/activeRunSelfHeal.js +79 -8
  11. package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
  12. package/dist/adapter/downstream/eventToSdkMessage.js +50 -9
  13. package/dist/adapter/downstream/terminalToSdkResult.d.ts +29 -0
  14. package/dist/adapter/downstream/terminalToSdkResult.js +46 -15
  15. package/dist/adapter/runStream.d.ts +22 -2
  16. package/dist/adapter/runStream.js +139 -38
  17. package/dist/adapter/types.d.ts +4 -1
  18. package/dist/autoModeUnavailable.d.ts +17 -9
  19. package/dist/autoModeUnavailable.js +26 -8
  20. package/dist/classifierStatus.d.ts +32 -4
  21. package/dist/classifierStatus.js +5 -3
  22. package/dist/controlRouter.d.ts +16 -0
  23. package/dist/controlRouter.js +6 -0
  24. package/dist/engineErrorCodes.d.ts +52 -0
  25. package/dist/engineErrorCodes.js +117 -0
  26. package/dist/engineNoticeCodes.d.ts +95 -1
  27. package/dist/engineNoticeCodes.js +124 -1
  28. package/dist/gateVocabulary.d.ts +18 -7
  29. package/dist/gateVocabulary.js +21 -8
  30. package/dist/hitl/parkResolver.d.ts +0 -14
  31. package/dist/hitl/parkResolver.js +22 -9
  32. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  33. package/dist/hitl/toolApprovalWire.js +1 -0
  34. package/dist/ownKey.d.ts +34 -0
  35. package/dist/ownKey.js +36 -0
  36. package/dist/request/taskRequest.d.ts +6 -6
  37. package/dist/request/taskRequest.js +45 -0
  38. package/dist/retryStatus.d.ts +13 -2
  39. package/dist/retryStatus.js +4 -1
  40. package/dist/runTerminal.d.ts +87 -14
  41. package/dist/runTerminal.js +89 -15
  42. package/dist/seam.d.ts +78 -8
  43. package/dist/seam.js +16 -2
  44. package/dist/toolResult.js +8 -0
  45. package/dist/toolRoster.d.ts +34 -2
  46. package/dist/toolRoster.js +16 -2
  47. package/dist/workflowClient.d.ts +22 -0
  48. package/dist/workflowClient.js +37 -0
  49. package/docs/INTEGRATION-CLIENTS.md +633 -9
  50. package/package.json +2 -2
@@ -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;
@@ -61,6 +61,9 @@
61
61
  * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
62
62
  * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
63
63
  */
64
+ // 🔴 0.68.0 / L-245:形制从 `readonly string[]` 改成**字面元组**(`as const`)—— 型面交得出成员
65
+ // 字面量,端的型面才能**派生**而不是再抄一遍两个词(与 `runTerminal.ts` 的两表分源同一条medicine)。
66
+ // 冻结的理由一字未改(公面消费者拿到的正是这个实例)。
64
67
  export const CLASSIFIER_DENY_CAUSES = Object.freeze(['unavailable', 'parse_error']);
65
68
  /** {@link CLASSIFIER_DENY_CAUSES} 的运行期成员判据(判据用它,别在端上再抄一张表)。 */
66
69
  const DENY_CAUSE_SET = new Set(CLASSIFIER_DENY_CAUSES);
@@ -103,6 +106,8 @@ export function classifierDenyCauseOf(gate) {
103
106
  const dd = d;
104
107
  if (dd.kind !== 'denied')
105
108
  return undefined;
109
+ // 🔴 0.68.0 / L-245:出参改**闭集成员型**(谓词已是型守卫)—— 读器闭集进、闭集出,措辞铸点
110
+ // 那一层的入参因此也能收窄,那条「结构不可达的兜底」于是变成编译期就闭的事(见下面那段)。
106
111
  return isClassifierDenyCause(dd.cause) ? dd.cause : undefined;
107
112
  }
108
113
  /**
@@ -142,6 +147,11 @@ export const AUTO_MODE_UNAVAILABLE_CAUSES = Object.freeze(['error', 'timeout']);
142
147
  * 重试解决不了,要去看那一轮的裁决散文 / 调分类器。
143
148
  * 🔴 两句**不许合并**:合并等于把「稍后重试」与「别重试」渲成同一句。
144
149
  */
150
+ // 🔴 0.68.0 / L-245:表型从 `Record<string, string>` 改成 **`Record<ClassifierDenyCause, string>`**。
151
+ // 这一改就是本件真正的「never 分支断言」:core 哪天加第三个词,元组长一员 ⇒ 成员型多一员 ⇒
152
+ // **这张表少一个键** ⇒ **编译期当场红**,逼人同批补那一句话。修前是 `Record<string,…>`,加词那天
153
+ // 这里一声不响,靠的是运行期那句「a word newer than this client」的兜底 —— 而那句兜底**结构上
154
+ // 走不到**(读器在上一层就按闭集把表外词判成缺席),所以它既拦不住漂移、也从来没说过话。
145
155
  const DENY_CAUSE_SENTENCES = Object.freeze({
146
156
  unavailable: 'denied because the auto-mode classifier could not run this round (the call may be retried later)',
147
157
  parse_error: 'denied because the auto-mode classifier answered outside its contract (its reply could not be parsed, so the call was blocked)',
@@ -151,18 +161,26 @@ const DENY_CAUSE_SENTENCES = Object.freeze({
151
161
  *
152
162
  * 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
153
163
  * 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
154
- * 🔴 表外词 / 坏值 ⇒ 一句**兜底**:仍然告诉用户「这次拒是分类器那条腿引出来的」,但**不冒充**
155
- * 两句里的任何一句;原样带上那个词供运维追问上游。
156
- * ⚠️ 与 {@link classifierDenyCauseOf} 的闭集判据**不矛盾**:那一层答「这条事实在不在」(闭集,
157
- * 出集 = 坏记录),本层答「拿到一个词怎么渲」——**渲判据面**的消费端(以及一条从旧持久态恢复
158
- * 回来的视图)可能手里就是一个表外词,它必须渲出一句诚实的话,而不是整屏崩或冒充一句已知的。
164
+ * ── 🔴 0.68.0 / L-245:入参从 `unknown` 收窄成**闭集成员型** ─────────────────────────────────
165
+ * 修前这里留着一句「its reported cause X **is a word newer than this client**」的兜底,而它
166
+ * **结构上走不到**:唯一到达本铸点的路是 {@link classifierDenyCauseOf},那一层已经按闭集把表外词
167
+ * 判成了缺席(理由见该函数:出闭集的 cause 在 core 那边是**记录缺陷**,server 整条不上帧 ⇒ 一个
168
+ * 表外词根本到不了消费端)。一条走不到的兜底有两重坏处:① 它假装这一面是开集,于是没人给这张表
169
+ * 配编译期围栏,core 加词那天这里一声不响;② 它说的那句话是**假的** —— 真有一个「比这一端新」的词
170
+ * 时,它压根不会到这里。
171
+ * ⇒ 入参收窄(表外词现在是**编译期**错误)+ 表型 `Record<ClassifierDenyCause, string>`(加词当天
172
+ * 缺键红)。剩下的运行期兜底只服务一种情形:调用方 cast 绕过型面、或从旧持久态恢复出一个非成员值
173
+ * —— 那时它说的是「这个词不在本端的闭集里」(一句真话),而**不再**冒充「上游比我新」。
174
+ * 🔴 渲染路径**不许抛**(本仓已定谳的病形),所以 never 分支照样交一句话,不 throw。
159
175
  */
160
176
  export function classifierDenyCauseDetail(cause) {
161
- const known = typeof cause === 'string' && Object.hasOwn(DENY_CAUSE_SENTENCES, cause)
177
+ // 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,一个被
178
+ // cast 进来的 `constructor` / `toString` 会命中 `Object.prototype` 上的**函数**并被当成一句话。
179
+ const known = Object.hasOwn(DENY_CAUSE_SENTENCES, cause)
162
180
  ? DENY_CAUSE_SENTENCES[cause]
163
181
  : undefined;
164
- if (known !== undefined)
182
+ if (typeof known === 'string')
165
183
  return known;
166
184
  const word = typeof cause === 'string' && cause.length > 0 ? cause : '(none)';
167
- return `denied by the auto-mode classifier lane; its reported cause ${word} is a word newer than this client`;
185
+ return `denied by the auto-mode classifier lane; its reported cause ${word} is not one of the causes this client's closed set knows`;
168
186
  }
@@ -20,6 +20,32 @@
20
20
  * `AUTO_MODE_UNAVAILABLE_CAUSES` 的理由)。
21
21
  */
22
22
  export declare const CLASSIFIER_STATUS_STATES: readonly string[];
23
+ /**
24
+ * 「本轮那一次观测」的**窄型面**(0.68.0 / L-245 B6)。
25
+ *
26
+ * ── 为什么这个型要有名字 ────────────────────────────────────────────────────────────────────
27
+ * 修前第二参是裸 `unknown`,而端手里常常只有**两格裸串**(`disposition.kind` / `disposition.cause`)——
28
+ * 于是壳把它们**铸回一个假门记录** `{gate:{disposition:{kind,cause}}}` 再喂进来(cli
29
+ * `classifierRoundObservation.ts` 的那一处)。那是一条「为了过读器而伪造上游形状」的路:伪造出来的
30
+ * 那层 `gate` 在 wire 上根本不存在,读者会以为端手里有一整只门记录,而下一次读器换键路时端还得
31
+ * 跟着改自己的伪造件。
32
+ * ⇒ 给它一个名字,并把**三条合法入形**写进型面:
33
+ * · `{ disposition }` —— 端手里只有处置那一格时**直接给这一格**(不必再铸一层 `gate`);
34
+ * · `{ gate }` —— 整只 `tool_end` 帧 / 一条耐久 park 行(键路 `gate.disposition`);
35
+ * · `{ origin }` —— 「分类器真的跑过」那条**肯定事实**的载体。
36
+ * 🔴 索引签名是**故意**的:整只 wire 帧(键远不止这三个)照样喂得进来,端零改造;而 `unknown`
37
+ * 类型的变量从此喂不进来 —— 那正是「端先自己判一下手里是什么」的那一步,也是伪造件消失的地方。
38
+ */
39
+ export interface ClassifierRoundObservation {
40
+ /** 门记录上的处置(`{kind:'denied', cause}`)。端手里只有这一格时直接给它。 */
41
+ readonly disposition?: unknown;
42
+ /** 整只帧 / 耐久行上的门记录(键路 `gate.disposition`)。 */
43
+ readonly gate?: unknown;
44
+ /** 这只 ask 的出身词(`askOriginOf` 读它;`CLASSIFIER_RAN_ORIGINS` 的成员 = 分类器真跑过)。 */
45
+ readonly origin?: unknown;
46
+ /** 整只 wire 帧上的其余键(读器一个都不读;留索引签名是为了端零改造)。 */
47
+ readonly [extra: string]: unknown;
48
+ }
23
49
  /** 一次状态读数。 */
24
50
  export interface ClassifierStatusView {
25
51
  /** {@link CLASSIFIER_STATUS_STATES} 之一。 */
@@ -31,9 +57,11 @@ export interface ClassifierStatusView {
31
57
  * 「这个会话上,auto 分类器现在是什么状态」——三态,或 `undefined`(**说不出来**)。
32
58
  *
33
59
  * @param autoMode `wiring_manifest` 的 `autoMode` 段(投影后的或原始的都吃;本函数自己窄读)
34
- * @param ask 可选:**本轮那一次观测** —— 一只 ask / 一条 durable park 行的 `tool_approval`
35
- * 载荷 / 一只带 `gate` 的 `tool_end` 帧 / 一条门记录本体。三形键路同形,同一把读器吃(0.67.0:
36
- * 否定事实的载体从 ask 上的 `classifierUnavailable` 改成门记录上的 `disposition.cause`)。
60
+ * @param ask 可选:**本轮那一次观测**({@link ClassifierRoundObservation};0.68.0 / L-245 起
61
+ * 有名字)—— 一只 ask / 一条 durable park 行的 `tool_approval` 载荷 / 一只带 `gate` 的 `tool_end`
62
+ * 帧 / 一条门记录本体 / **只有 `{disposition}` 那一格**。键路同形,同一把读器吃(0.67.0:否定事实
63
+ * 的载体从 ask 上的 `classifierUnavailable` 改成门记录上的 `disposition.cause`)。
64
+ * 🔴 端**不必**为了过这只读器去铸一个假门记录:手里只有两格裸串时,直接交 `{disposition:{kind,cause}}`。
37
65
  *
38
66
  * 优先序(承重,理由见模块顶注):**本轮事实(否定 + 肯定)> 这条腿的 `armed`**——先看观测座上的
39
67
  * 本轮不可用事实,再看那条「分类器真的跑过」的肯定事实,最后才看这条腿武没武装。
@@ -45,7 +73,7 @@ export interface ClassifierStatusView {
45
73
  * 两种情形都**绝不**折成 `available`(那是把「没报」渲成「一切正常」)。
46
74
  * ⚠️ 「没武装」本身仍是一条要渲的事实 —— 但它的出处是 `autoMode.reason`,不是本读器。
47
75
  */
48
- export declare function classifierStatusOf(autoMode: unknown, ask?: unknown): ClassifierStatusView | undefined;
76
+ export declare function classifierStatusOf(autoMode: unknown, ask?: ClassifierRoundObservation): ClassifierStatusView | undefined;
49
77
  /**
50
78
  * 一次状态读数 → 一句人话。**唯一措辞铸点**(三端共用;端零自拼)。
51
79
  *
@@ -135,9 +135,11 @@ const NOT_A_ROUND_FAILURE = new Set(['parse_error']);
135
135
  * 「这个会话上,auto 分类器现在是什么状态」——三态,或 `undefined`(**说不出来**)。
136
136
  *
137
137
  * @param autoMode `wiring_manifest` 的 `autoMode` 段(投影后的或原始的都吃;本函数自己窄读)
138
- * @param ask 可选:**本轮那一次观测** —— 一只 ask / 一条 durable park 行的 `tool_approval`
139
- * 载荷 / 一只带 `gate` 的 `tool_end` 帧 / 一条门记录本体。三形键路同形,同一把读器吃(0.67.0:
140
- * 否定事实的载体从 ask 上的 `classifierUnavailable` 改成门记录上的 `disposition.cause`)。
138
+ * @param ask 可选:**本轮那一次观测**({@link ClassifierRoundObservation};0.68.0 / L-245 起
139
+ * 有名字)—— 一只 ask / 一条 durable park 行的 `tool_approval` 载荷 / 一只带 `gate` 的 `tool_end`
140
+ * 帧 / 一条门记录本体 / **只有 `{disposition}` 那一格**。键路同形,同一把读器吃(0.67.0:否定事实
141
+ * 的载体从 ask 上的 `classifierUnavailable` 改成门记录上的 `disposition.cause`)。
142
+ * 🔴 端**不必**为了过这只读器去铸一个假门记录:手里只有两格裸串时,直接交 `{disposition:{kind,cause}}`。
141
143
  *
142
144
  * 优先序(承重,理由见模块顶注):**本轮事实(否定 + 肯定)> 这条腿的 `armed`**——先看观测座上的
143
145
  * 本轮不可用事实,再看那条「分类器真的跑过」的肯定事实,最后才看这条腿武没武装。
@@ -81,6 +81,10 @@ export interface ControlClientLike {
81
81
  * 可达其中 4 个:`not_running`(409)/ `invalid_content`(422)/ `queue_full`(409,core 5.14.0 队列
82
82
  * 化后新出)/ `duplicate_input_id`(409,调用方带 `Idempotency-Key` 时可达);另外 3 个来自子代
83
83
  * steer/resume 面与 workflow agent steer 面(`ambiguous_target` / `ambiguous_label` / `still_running`)。
84
+ * 🔴 **[7226] 包侧缺口 ①(0.68.1)**:码表补**第 8 码** `steering.blocked_by_hook`(422,部署
85
+ * `userPromptSubmit` 门拦下;live 腿可达)。它此前落开集兜底位 `steering_other` —— 兜底位的判词
86
+ * 逐字是「别按成员猜它的意思」,而这一码的处置恰恰是**明确的**(输入未受理、改内容自由重试)⇒
87
+ * 兜底在这一位上不是「安全降级」,是把一条能自救的拒绝渲成一条不知道怎么办的拒绝。
84
88
  * SDK 6.3.0 侧已把整族改成**前缀分派**(`SteeringError` 基类),所以「没认全」的后果不是崩溃,
85
89
  * 而是那些码原样裸抛给壳 —— 壳只 `catch (e instanceof ControlSafetyError)` 就漏在外面。
86
90
  *
@@ -101,6 +105,18 @@ export type ControlSafetyCode =
101
105
  | 'ambiguous_target'
102
106
  /** 对一个**还在飞**的子代调了 resume;处置=改调 steer,或等它 settle。`not_running` 的反面。 */
103
107
  | 'still_running'
108
+ /**
109
+ * 部署的 `userPromptSubmit` 门拦下了这条输入([7226] 包侧缺口 ①,0.68.1;server 契约 (2) 表
110
+ * 第 8 行;core 5.62 design/373 §4.3)。block / 超时 / 崩溃**同码 fail-closed**,成因由 message
111
+ * 判别(携 hook 自己的 bounded reason)。
112
+ * 🔴 **输入未被受理**:没有 `human_input` 帧、`inputId` 不入账 ⇒ 处置 = **改内容自由重试**。
113
+ * 🔴 它与 `invalid_content` / `steering_other` 都**不许合并**:前者是「正文违规,SURFACE 别
114
+ * strip-and-retry」,后者的判词逐字是「别按成员猜它的意思」—— 把一条**能自救**的拒绝塌进这两位
115
+ * 任何一位,UI 都会把「改一句话再发」说成「你没救了」。
116
+ * ⚠️ `/steer` 的 **park 腿结构性不可达本码**(hook 对 parked 转向的拦截发生在 resume 再投递时刻,
117
+ * 走 `steering.parked_input_blocked` 通告);本路由器调的是 live 腿,所以这一位在这里可达。
118
+ */
119
+ | 'blocked_by_hook'
104
120
  /** 开集兜底:`steering.` 前缀但本表不认得的**未来**码(SDK 前缀分派同款姿势)。
105
121
  * 处置=按「这条 steer 没落地」呈现,并把 `cause.errorCode` 原样打进日志,别按成员猜语义。 */
106
122
  | 'steering_other'
@@ -243,6 +243,11 @@ const STEERING_CODE_TO_SAFETY = new Map([
243
243
  ['steering.ambiguous_target', 'ambiguous_target'],
244
244
  ['steering.ambiguous_label', 'ambiguous_target'],
245
245
  ['steering.still_running', 'still_running'],
246
+ // [7226] 包侧缺口 ①(0.68.1):server 契约 (2) 表第 8 码(422)。SDK 8.8.0 **没有**专属子类
247
+ // (`errors.d.ts` 的 `Steering*Error` 只有六只)⇒ 只能按 `errorCode` 认;认不出时它会落开集位
248
+ // `steering_other`,而那一位的判词是「别猜它的意思」—— 恰好把一条「改内容重试即可」的拒绝
249
+ // 说成没救。所以码表必须点名它,不能靠前缀兜底。
250
+ ['steering.blocked_by_hook', 'blocked_by_hook'],
246
251
  ]);
247
252
  /** SDK 的 typed 子类名兜底(错误对象被传输层剥掉 `errorCode` 时仍认得族;`SteeringError` 基类本身
248
253
  * = 「是 steering 族但没有专属子类」⇒ 落开集臂)。 */
@@ -261,6 +266,7 @@ const STEERING_ADVICE = new Map([
261
266
  ['duplicate_input_id', 'this Idempotency-Key is already parked with DIFFERENT steering content — reissue with a fresh key, resending verbatim will never succeed'],
262
267
  ['ambiguous_target', 'more than one live target matches — address it uniquely (sub-agent: parentToolCallId; workflow: a more specific label); retrying verbatim yields the same result'],
263
268
  ['still_running', 'the target is STILL RUNNING — steer it instead of resuming, or wait for it to settle (this is the exact opposite of not_running: never collapse the two)'],
269
+ ['blocked_by_hook', "this deployment's userPromptSubmit gate refused the input (block, timeout and crash all report this one code, fail-closed) — the input was NOT accepted: no human_input frame, the inputId is not on the ledger, so editing the text and sending again is a normal retry, not a duplicate"],
264
270
  ['steering_other', 'an unrecognized steering.* refusal (open set) — the steer did NOT land; log the errorCode verbatim, do not guess its meaning'],
265
271
  ]);
266
272
  /**
@@ -283,6 +283,37 @@ export declare const DECIDE_WORKFLOW_REMEMBER_UNSUPPORTED = "decide.workflow_rem
283
283
  * 🔴 加成员必须**同批**补那一句人话 + 判据,不许靠 `decide.` 前缀放宽(前缀下住着三种处置)。
284
284
  */
285
285
  export declare const DECIDE_WORKFLOW_LANE_CODES: readonly string[];
286
+ /** 这条 workflow 的 park 真相**读不出来**(店抛了 / 超时 / 记录不在 / 记录还在跑)。
287
+ * `detail.reason` 开集(`checkpoint_store_threw` / `store_threw` / `record_missing` /
288
+ * `record_not_terminal`…;词表属主在 core)⇒ 读得出即原样带,读不懂**不折**已知词。 */
289
+ export declare const WORKFLOW_PARK_TRUTH_UNREADABLE = "workflow.park_truth_unreadable";
290
+ /** 记录说这一序号 park 着,而 checkpoint 店说那只审批**已决 / 已过期 / 被回收**。
291
+ * `detail.status` 三词(`absent` / `expired` / `resolved`)+ `detail.ordinal`;
292
+ * `detail.checkpointId` **可缺席**(店里没 id)—— 缺席 ≠ 「没有 checkpoint」。 */
293
+ export declare const WORKFLOW_PARK_NOT_PENDING = "workflow.park_not_pending";
294
+ /** 记录里的 park **绑不回**它的 checkpoint(记录自相矛盾)。`detail.ordinal` 在场。 */
295
+ export declare const WORKFLOW_PARK_BINDING_BROKEN = "workflow.park_binding_broken";
296
+ /** 这条 workflow 的子代会 park,而部署**没有 run 店**(或没有 checkpoint 店)⇒ 没有耐久行可供
297
+ * 宿主按它路由那只停着的审批。`detail` 恒空对象(这是**配置**问题,不是某一条 run 的事实)。 */
298
+ export declare const WORKFLOW_PARK_REQUIRES_RUN_STORE = "workflow.park_requires_run_store";
299
+ /**
300
+ * workflow **park 真相**拒绝码的闭集(四员;core 7.17.0 `workflow.ts:602` 码集)。
301
+ *
302
+ * 🔴 闭的是「**这四个码是 park 真相这一族独有的**」,不是「`workflow.*` 一共有几个码」——
303
+ * 后者仍是开集(`workflow.governance_key_stripped` / `workflow.agent_option_ignored` 是通告码,
304
+ * `workflow.journal_incompatible` 是另一族拒绝码,各有各的既有处置)。
305
+ * 🔴 **绝不靠 `workflow.park_` 前缀放宽**:前缀是个命名巧合,不是契约;上游哪天加第五个码,
306
+ * 该在这里显形并被人处置,而不是被一条前缀判据无声吞进来。
307
+ * 🔴 形制:`Object.freeze` 的数组(同 {@link DECIDE_WORKFLOW_LANE_CODES})。
308
+ */
309
+ export declare const WORKFLOW_PARK_REFUSAL_CODES: readonly string[];
310
+ /**
311
+ * 这个码是不是 workflow park 真相族的拒绝码({@link WORKFLOW_PARK_REFUSAL_CODES} 的成员)。
312
+ *
313
+ * 🔴 表外码 ⇒ `false` = 「本端认不出它属于这一族」,**不是**「这次 resume 没问题」。
314
+ * 🔴 非串 / 空串 ⇒ `false`;认**原始值**(消毒只进文案,不进判据)。
315
+ */
316
+ export declare function isWorkflowParkRefusalCode(code: unknown): boolean;
286
317
  /**
287
318
  * server 温切 drain 门的 pre-stream 拒收码(503 + `errorCode:"draining"`;server 侧
288
319
  * `error:"draining"` 是冻结契约,SDK toApiError 盖成 `errorCode`)。此前壳/包注释各持裸字面。
@@ -314,3 +345,24 @@ export type ModelFallbackReason = 'inherit_no_tier_binding';
314
345
  export declare const MODEL_FALLBACK_INHERIT_NO_TIER_BINDING: ModelFallbackReason;
315
346
  /** wire 上的 `modelFallback` 窄化:是闭集成员才认,别的一律当缺席(不认得的原因 ≠ 编一个)。 */
316
347
  export declare function asModelFallbackReason(v: unknown): ModelFallbackReason | undefined;
348
+ /**
349
+ * 引擎 turn 停止原词的**识别表**(五词;core `StopReason` 归一化后的值集)。
350
+ *
351
+ * 🔴 **它是识别表不是闭集判据**:`turn_end.stopReason` 在 sdk 型面上是开放 `string`(上游刻意让
352
+ * 引擎的 union 保持私有)⇒ 引擎比这一端新时真的会送一个第六个词过来。本表回答的是
353
+ * 「我认不认得这个词」,**绝不是**「合法的词只有这五个」。
354
+ */
355
+ export declare const ENGINE_STOP_REASONS: readonly string[];
356
+ /**
357
+ * 引擎停止原词 → CC `stop_reason` 的**唯一映射口**。**三态**,刻意不合并:
358
+ * · `string` —— 映到了一个 CC 词;
359
+ * · `null` —— 映到了**诚实缺席**(`error` / `aborted`:CC 没有对应词,不编一个);
360
+ * · `undefined` —— **这个词本端不认识**(表外词 / 非串 / 空串)⇒ 调用方按「不映射」走原路。
361
+ *
362
+ * 🔴 `null` 与 `undefined` 必须分开:前者是「我认得这个词,而它的正确渲法就是没有词」,后者是
363
+ * 「我不认得这个词」。合成一个值 ⇒ 引擎加第六个词那天,它会被渲成一次「这一轮没有 stop_reason」
364
+ * 的**肯定事实**,而真相是这一端读不懂 —— 本仓反复在修的那条「把不知道渲成事实」的病。
365
+ * 🔴 **按自有属性查表**(`Object.freeze` 不移除原型):一个来自 wire 的 `constructor` / `toString`
366
+ * 会命中 `Object.prototype` 上的**函数**并被当成一个 CC 词交出去。
367
+ */
368
+ export declare function engineStopReasonToCc(stopReason: unknown): string | null | undefined;