@sema-agent/client-core 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
23
23
 
24
24
  ## Scope
25
25
 
26
- **Version:** 0.19.0
26
+ **Version:** 0.20.1
27
27
 
28
28
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
29
29
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -53,7 +53,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
53
53
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
54
54
  five modules, and the browser bundle really bundles the SDK through (the portability guard would
55
55
  exit 3 rather than quietly mark it external).
56
- - The declared floor is `>=6.3.0`, and it is *witnessed*: the guard checks that an actually
56
+ - The declared floor is `>=6.9.0`, and it is *witnessed*: the guard checks that an actually
57
57
  installed SDK at that line still exports every value-level symbol this package imports and still
58
58
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
59
59
  promise, not a contract.
@@ -63,7 +63,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
63
63
  uuid: idOf(m),
64
64
  session_id: ctx.sessionId,
65
65
  parent_tool_use_id: null,
66
- });
66
+ }, ctx.now());
67
67
  if (decision === 'reject' || decision === 'cancel') {
68
68
  // S1:一个结果关闭该 model response。**yield 之前**复位 —— 矩阵 §2.2 定性为
69
69
  // 「形不一致的技术债、不是缺陷」(L1 turn 级、下一句就 return、早退时无读者),
@@ -84,7 +84,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
84
84
  },
85
85
  uuid: idOf(m, decision),
86
86
  session_id: ctx.sessionId,
87
- });
87
+ }, ctx.now());
88
88
  return;
89
89
  }
90
90
  if (decision === 'error') {
@@ -104,7 +104,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
104
104
  },
105
105
  uuid: idOf(m, 'error'),
106
106
  session_id: ctx.sessionId,
107
- });
107
+ }, ctx.now());
108
108
  return;
109
109
  }
110
110
  // ⟨帧序耦合 1/7 + 3/7⟩ 开卡:后到的 task_progress 才猜得到父、后到的 tool_end 才关得掉卡。
@@ -130,7 +130,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
130
130
  uuid: idOf(m),
131
131
  session_id: ctx.sessionId,
132
132
  parent_tool_use_id: null,
133
- });
133
+ }, ctx.now());
134
134
  text.markEmittedText();
135
135
  }
136
136
  const thinkingBlock = blocks.find(b => b?.type === 'thinking');
@@ -141,7 +141,7 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
141
141
  uuid: idOf(m, 'durable-thinking'),
142
142
  session_id: ctx.sessionId,
143
143
  parent_tool_use_id: null,
144
- });
144
+ }, ctx.now());
145
145
  }
146
146
  };
147
147
  // ══ M5 纯投影臂(零适配器状态,类型即证明)══════════════════════════════════════════════════════
@@ -149,10 +149,10 @@ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
149
149
  const userArm = function* (m, { ctx, idOf }) {
150
150
  if (m.message === undefined)
151
151
  return;
152
- yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
152
+ yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId }, ctx.now());
153
153
  };
154
- const systemArm = function* (m, { idOf }) {
155
- yield transcript(m);
154
+ const systemArm = function* (m, { ctx, idOf }) {
155
+ yield transcript(m, ctx.now());
156
156
  // core 1.219 attachWorkingFiles:compact_boundary 可携 attachedFiles → CC 的
157
157
  // "Referenced file {path}" 行(post-compact attachment,零新 UI)。
158
158
  const attachedFiles = m.attachedFiles;
@@ -212,6 +212,47 @@ const workspaceChangedArm = function* (m, { idOf }) {
212
212
  attachment: { type: 'workspace_changed', cwd },
213
213
  });
214
214
  };
215
+ /**
216
+ * FIX⑦(2026-08-07,黑板 [3017]/[3020])—— `human_input` 生命周期账本帧。
217
+ *
218
+ * 姿势抄同族的 `steering_injected` / `workspace_changed`(都是「引擎侧发生了一件事,壳记一笔」的
219
+ * 非持久帧,归 chrome),但**不走 `attachment` 臂**:那三条 attachment 是要在转录流里渲出一行给
220
+ * 用户看的东西,而本帧**不带正文** —— 没有可渲的内容。硬造一行「有人输入了什么」的空 attachment
221
+ * 就是在编内容。所以它有自己的 chrome 臂,宿主义务是「可选地记进审计/时间线」。
222
+ * 子流帧(带 parentToolCallId)同族让位:不上 leader 面。
223
+ */
224
+ const humanInputArm = function* (m) {
225
+ if (typeof m.parentToolCallId === 'string')
226
+ return;
227
+ const carrier = typeof m.carrier === 'string' ? m.carrier : '';
228
+ if (carrier.length === 0)
229
+ return; // 无载体的账本行渲出去只是噪声(投影层已判 malformed,这里是第二道)
230
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
231
+ const rawActor = typeof m.actor === 'object' && m.actor !== null && !Array.isArray(m.actor)
232
+ ? m.actor
233
+ : undefined;
234
+ const actor = rawActor === undefined
235
+ ? undefined
236
+ : {
237
+ ...(str(rawActor.id) !== undefined ? { id: str(rawActor.id) } : {}),
238
+ ...(typeof rawActor.hostAsserted === 'boolean' ? { hostAsserted: rawActor.hostAsserted } : {}),
239
+ ...(str(rawActor.issuer) !== undefined ? { issuer: str(rawActor.issuer) } : {}),
240
+ };
241
+ yield chrome({
242
+ kind: 'human_input',
243
+ laneProof: MAIN,
244
+ carrier,
245
+ ...(str(m.inputId) !== undefined ? { inputId: str(m.inputId) } : {}),
246
+ ...(typeof m.sessionSeq === 'number' && Number.isFinite(m.sessionSeq) ? { sessionSeq: m.sessionSeq } : {}),
247
+ ...(str(m.source) !== undefined ? { source: str(m.source) } : {}),
248
+ ...(str(m.delivery) !== undefined ? { delivery: str(m.delivery) } : {}),
249
+ ...(str(m.issuer) !== undefined ? { issuer: str(m.issuer) } : {}),
250
+ ...(actor !== undefined && Object.keys(actor).length > 0 ? { actor } : {}),
251
+ ...(str(m.entryId) !== undefined ? { entryId: str(m.entryId) } : {}),
252
+ // 宿主幂等落账的键(codex 复审二轮):重放同一条账本帧必须得到同一个 eventId。
253
+ ...(str(m.eventId) !== undefined ? { eventId: str(m.eventId) } : {}),
254
+ });
255
+ };
215
256
  const promptSuggestionsArm = function* (m) {
216
257
  // T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
217
258
  if (typeof m.parentToolCallId === 'string')
@@ -271,7 +312,7 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
271
312
  timestamp: new Date(ctx.now()).toISOString(),
272
313
  uuid: idOf(m, 'memory-saved'),
273
314
  isMeta: false,
274
- });
315
+ }, ctx.now());
275
316
  }
276
317
  }
277
318
  // ── E 层 ②:core 1.217 task 族 structured —— 权威全量态精确同步 ctrl+t 面板(引擎真实 id)。
@@ -467,7 +508,7 @@ const resultArm = function* (m, { ctx, idOf, text, cards, panel, flags }) {
467
508
  uuid: idOf(m, 'terminal-text'),
468
509
  session_id: ctx.sessionId,
469
510
  parent_tool_use_id: null,
470
- });
511
+ }, ctx.now());
471
512
  text.markEmittedText();
472
513
  }
473
514
  }
@@ -556,7 +597,7 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
556
597
  message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
557
598
  uuid: idOf(m),
558
599
  session_id: ctx.sessionId,
559
- });
600
+ }, ctx.now());
560
601
  };
561
602
  /**
562
603
  * ⚠️ 本臂声明成 `ProjectionArmFn`(碰不到五个适配器状态模块),但它**有副作用** ——
@@ -602,15 +643,29 @@ const retryStatusArm = function* (m, { ctx, flags }) {
602
643
  // #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
603
644
  if (typeof m.parentToolCallId === 'string')
604
645
  return;
605
- yield chrome({
606
- kind: 'retry_status',
607
- laneProof: MAIN,
608
- status: mapBrainStatusToRetry({
609
- phase: typeof m.phase === 'string' ? m.phase : '',
610
- ...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
611
- ...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
612
- }, ctx.now()),
613
- });
646
+ // 🔴 六键全转(#3004 跟修批 2026-08-08):此前这里只挑 phase/detail/retryInSec 三键,attempt /
647
+ // maxRetries / retryInMs 被剥在本层 —— 而 server 两腿的 brainStatusEventData 是全发的,
648
+ // `RetryStatus` 上也早就留好了 attempt/maxRetries 两位。键集真源 =
649
+ // `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对实装 core 对账)。
650
+ const status = mapBrainStatusToRetry({
651
+ phase: typeof m.phase === 'string' ? m.phase : '',
652
+ ...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
653
+ ...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
654
+ ...(typeof m.retryInMs === 'number' ? { retryInMs: m.retryInMs } : {}),
655
+ ...(typeof m.attempt === 'number' ? { attempt: m.attempt } : {}),
656
+ ...(typeof m.maxRetries === 'number' ? { maxRetries: m.maxRetries } : {}),
657
+ }, ctx.now());
658
+ // `status === null` = 引擎直报 `recovered`(重试成功)。这一拍是**清除腿之一** —— 但间接腿
659
+ // (TurnFlags 的「主 lane 内容帧到达即清」)可能已经清过了:已安装 core 在内容流跑完的 finally
660
+ // 里发 `recovered`,所以 `retrying → 内容 → recovered` 才是常态顺序。故用 `takeRetryOverlay()`
661
+ // 的**原子读落**决定要不要发:旗还在 ⇒ 发这一发并落旗;旗已落 ⇒ 沉默(没有覆盖层可摘)。
662
+ // 首版写成「无条件发 + clearRetryOverlay」,在常态顺序上双清(codex 复审命中)。
663
+ if (status === null) {
664
+ if (flags.takeRetryOverlay())
665
+ yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
666
+ return;
667
+ }
668
+ yield chrome({ kind: 'retry_status', laneProof: MAIN, status });
614
669
  // ⟨帧序耦合 6/7⟩ 挂旗;下一个主 lane 内容帧到达时由 TurnFlags 的帧前置清掉(恢复即清)。
615
670
  flags.noteRetryOverlay();
616
671
  };
@@ -632,6 +687,7 @@ export const ARMS = new Map([
632
687
  ['diagnostics', diagnosticsArm],
633
688
  ['steering_injected', steeringInjectedArm],
634
689
  ['workspace_changed', workspaceChangedArm],
690
+ ['human_input', humanInputArm],
635
691
  ['prompt_suggestions', promptSuggestionsArm],
636
692
  ['retry_status', retryStatusArm],
637
693
  ]);
@@ -13,7 +13,7 @@ export declare const chrome: (event: ChromeEvent) => AdapterOutput;
13
13
  * 到 `SDKMessage` 的那一步仍是 cast:CC 词汇的字面形与 SDK 类型有已登记的分歧(DIVERGENCE-2/9),
14
14
  * 这条 cast 是**有意**的边界,不是偷懒。
15
15
  */
16
- export declare const transcript: (message: Frame) => AdapterOutput;
16
+ export declare const transcript: (message: Frame, nowMs: number) => AdapterOutput;
17
17
  /**
18
18
  * [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
19
19
  * `uuid` 字段(§E2 core 铸的 eventId / stamp 铸的帧 id),故映到 `id` 位喂进去(见 SEAM-GAP-1)。
package/dist/adapt/ids.js CHANGED
@@ -9,16 +9,18 @@ export const chrome = (event) => ({ plane: 'chrome', event });
9
9
  * 到 `SDKMessage` 的那一步仍是 cast:CC 词汇的字面形与 SDK 类型有已登记的分歧(DIVERGENCE-2/9),
10
10
  * 这条 cast 是**有意**的边界,不是偷懒。
11
11
  */
12
- export const transcript = (message) => ({
12
+ export const transcript = (message, nowMs) => ({
13
13
  plane: 'transcript',
14
14
  // 到达时戳补齐(#185-A1 正位收编,2026-08-08;此前是壳 upstreamBridge.stampTranscriptArrival
15
15
  // 的消费端补偿,注释明记「长线正位在 client-core 的 adapt 臂」):transcript 消息不变式=恒带
16
16
  // timestamp——CC 在 content_block_stop 取 wall-clock,而 transcript 帧正是块收尾时到达,到达
17
17
  // 时刻本身就是同一个量。只补缺席不覆盖(wire 哪天带了就用 wire 的);壳侧补偿臂保留为双保险
18
18
  // (its ③:不覆盖已有值 ⇒ 本补齐生效后壳臂自然零触发)。
19
+ // [C114] 请托(web jsdom-adapt-parity 门 INCONCLUSIVE 实抓):时钟**必经 ctx.now()**(调用方
20
+ // 注入 nowMs)——裸 `new Date()` 违 DIVERGENCE-7 可重放纪律,确定性双跑逐字节对拍当场失判别力。
19
21
  message: (typeof message.timestamp === 'string'
20
22
  ? message
21
- : { ...message, timestamp: new Date().toISOString() }),
23
+ : { ...message, timestamp: new Date(nowMs).toISOString() }),
22
24
  });
23
25
  /**
24
26
  * [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
@@ -35,7 +35,7 @@ export function createTextStream(ctx, idOf) {
35
35
  thinkingBlockOpen = false;
36
36
  // P2d:活体 "∴ Thinking…" 行在 committed 块落地的**同一刻**收掉(先收行再上消息,cli 同序)
37
37
  yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
38
- yield transcript(msg);
38
+ yield transcript(msg, ctx.now());
39
39
  }
40
40
  /** 提交累积的文本段(cli takeAnswerSegment:#27 段序铁律——工具卡前/turn 末各 flush 一次)。 */
41
41
  function* takeAnswerSegment() {
@@ -53,7 +53,7 @@ export function createTextStream(ctx, idOf) {
53
53
  segmentAnchor = null;
54
54
  emittedAssistantText = true;
55
55
  textBlockOpen = false;
56
- yield transcript(msg);
56
+ yield transcript(msg, ctx.now());
57
57
  }
58
58
  /** 合并后的活体增量下泄(cli drainLive:思考先于文本 = 持久臂序)。 */
59
59
  function* drainLive() {
@@ -68,7 +68,7 @@ export function createToolCardLedger(ctx, idOf) {
68
68
  toolUseResult: rich !== null ? rich.toolUseResult : (body.toolUseResult ?? body.content),
69
69
  // T7:wire 没带正文的诚实标记 —— 在场 = 这张卡没有真实执行结果(UI 显示决策留端)。
70
70
  ...(body.degraded !== undefined ? { _sema_degraded: body.degraded } : {}),
71
- });
71
+ }, ctx.now());
72
72
  }
73
73
  /** 关一张卡的**唯一**实现:出栈 + S1 复位 + 铸卡,前两步恒在第一个 yield 之前。 */
74
74
  function* closeCard(p, end) {
@@ -33,5 +33,19 @@ export interface TurnFlags {
33
33
  emitResultEndIfNeeded(readOutputTokens: () => number | undefined): Generator<AdapterOutput>;
34
34
  /** A15 `retry_status`:挂旗(下一帧主 lane 内容到达即清)。 */
35
35
  noteRetryOverlay(): void;
36
+ /**
37
+ * A15 `retry_status` 的反向动作(#3004 跟修批 2026-08-08):**引擎直报恢复**(`recovered` 相)那一拍
38
+ * 调它 —— 返回「本次是否**仍需**发清除」并同时落旗(**原子**:读+落一步完成)。
39
+ *
40
+ * 🔴 为什么必须是「take」而不是「clear」(codex 复审命中的双清):两条清除腿共用同一个布尔位,
41
+ * 而它们的触发顺序两个方向都真实存在 ——
42
+ * · `retrying → recovered`:直报腿先到,旗还在 ⇒ 本次要发清除,发完落旗;间接腿之后看不到旗。
43
+ * · `retrying → 主lane内容 → recovered`:**间接腿先到**(已发过一次 null 并落了旗),直报腿这一拍
44
+ * 就**不该再发第二发** —— 已安装 core 在内容流跑完的 finally 里发 `recovered`,所以这个顺序
45
+ * 才是常态路径。首版写成无条件发 + `clearRetryOverlay()`,于是这条常态路径上双清。
46
+ * 返回 false ⇒ 本臂这一拍不 yield(没有覆盖层可摘,沉默才是诚实的);状态机仍只有本文件一处
47
+ * 持有那个布尔位。
48
+ */
49
+ takeRetryOverlay(): boolean;
36
50
  }
37
51
  export declare function createTurnFlags(ctx: AdapterContext, turnStartAt: number): TurnFlags;
@@ -4,9 +4,15 @@ export function createTurnFlags(ctx, turnStartAt) {
4
4
  let endEmitted = false;
5
5
  /**
6
6
  * R1(clay 五报 07-31):retry 覆盖层的四个清除点全在 turn 边界,缺「重试成功→清」的转移 ——
7
- * 引擎重试成功那一刻什么都不发(BrainStatusPhase 无恢复终态,候 core [2163]),所以恢复只能由
8
- * 「主 lane 又有内容在流」这个事实来证明。挂旗后首个主 lane 内容帧即清,CC 同位 = 重试循环
9
- * 退出即 onRetryStatus(null)(pretty220 finally 清)。
7
+ * 当时引擎重试成功那一刻什么都不发,所以恢复只能由「主 lane 又有内容在流」这个事实来证明。
8
+ * 挂旗后首个主 lane 内容帧即清,CC 同位 = 重试循环退出即 onRetryStatus(null)(pretty220 finally 清)。
9
+ *
10
+ * 🔴 现势更正(2026-08-08,#3004 跟修批):上面括号里原本写着「BrainStatusPhase 无恢复终态,候
11
+ * core [2163]」—— core 早已兑现,`BrainStatusPhase` 现有 `recovered` / `gave_up` 两个终态相。于是
12
+ * 本位这条**间接推断腿**不再是唯一证据:`retry_status` 臂拿到 `recovered` 时走 `takeRetryOverlay()`
13
+ * 直报清除。两条腿共用同一个布尔位、且都靠 take 的**原子读落**决定要不要发帧,所以无论到达顺序
14
+ * 如何(直报先 / 内容帧先),一次恢复合计**恰好一发** `status:null`(pure 门 R1b 段两个方向都钉)。
15
+ * 间接腿保留 —— 老引擎不发 `recovered`,它仍是那些部署上唯一的证据。
10
16
  */
11
17
  let retryOverlayActive = false;
12
18
  return {
@@ -20,7 +26,8 @@ export function createTurnFlags(ctx, turnStartAt) {
20
26
  // stream_event 的子流标记在 event 里(thinking 臂同款判位),帧顶层没有。
21
27
  typeof m.event?.parentToolCallId !== 'string') {
22
28
  // 恢复即清:主 lane 内容帧到达 = 引擎那次重试已成功;子流帧(parentToolCallId)不算证据
23
- // (主 brain 仍可能在退避等待,#47 矩阵 #5 同族判别)
29
+ // (主 brain 仍可能在退避等待,#47 矩阵 #5 同族判别)。落旗后 `takeRetryOverlay()` 会对随后
30
+ // 到达的 `recovered` 直报帧返回 false ⇒ 不补第二发(两条腿合计恰好一发)。
24
31
  retryOverlayActive = false;
25
32
  yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
26
33
  }
@@ -51,5 +58,10 @@ export function createTurnFlags(ctx, turnStartAt) {
51
58
  noteRetryOverlay: () => {
52
59
  retryOverlayActive = true;
53
60
  },
61
+ takeRetryOverlay: () => {
62
+ const wasActive = retryOverlayActive;
63
+ retryOverlayActive = false;
64
+ return wasActive;
65
+ },
54
66
  };
55
67
  }
package/dist/adapt.d.ts CHANGED
@@ -50,7 +50,7 @@ export interface AdapterLedgerState {
50
50
  /** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
51
51
  export declare const ADAPTER_COVERAGE: {
52
52
  /** 已落码的帧臂。 */
53
- readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
53
+ readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "human_input", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
54
54
  /** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
55
55
  readonly transcriptKinds: readonly ["assistant_text", "assistant_thinking", "assistant_tool_use", "user_tool_result_decision", "user_task_notification", "system_passthrough"];
56
56
  /** B3(0.5.0)新落码的臂/半场 —— 从 todo 移过来的,别再在 todo 里留同名条目。 */
package/dist/adapt.js CHANGED
@@ -31,6 +31,8 @@ export const ADAPTER_COVERAGE = {
31
31
  'diagnostics',
32
32
  'steering_injected',
33
33
  'workspace_changed',
34
+ // FIX⑦(2026-08-07):`human_input` 账本帧 —— 投 chrome 记账,**不铸 transcript 行**(不带正文)。
35
+ 'human_input',
34
36
  'prompt_suggestions',
35
37
  'retry_status',
36
38
  'task_progress',
@@ -58,7 +58,18 @@ export type EventProjectionDropReason =
58
58
  * ⚠️ 「筛完是空的」不算畸形(那是 `empty_payload`,见上;ADAPTER-F6)。 */
59
59
  'malformed'
60
60
  /** 类型面根本不认识这条臂(引擎比本包新)。 */
61
- | 'unknown_arm';
61
+ | 'unknown_arm'
62
+ /**
63
+ * 类型面**认得**这条臂、它也带着用户可见的决策内容,但本包**今天没有任何消费口**
64
+ * (2026-08-08 codex 复审第 4/5 轮命中,首例 = `approval_request`)。
65
+ *
66
+ * 为什么不能并进 `not_in_slice`:那一档的语义是「本切片不投影,**因为别处接了/本来就没有可渲染物**」,
67
+ * 它走 `kind:'none'` —— 而 `none` 是**静默**的,只有 `dropped` 会经 `reportDroppedFrame` 走宿主
68
+ * sink + console 留痕。把一条「有内容、没人接」的帧塞进 `none`,等于把一个真实能力缺口做成静默
69
+ * fail-open(本仓 P0 共性病的同款形)。判据方向:宁可每次都吼一行,也不要缺口无声。
70
+ * 为什么也不能并进 `unknown_arm`:那是假话 —— 我们认得它,缺的是消费链,不是类型。
71
+ */
72
+ | 'unsupported_arm';
62
73
  /**
63
74
  * 投影结果三态(REF-CC-058 / xlate-04,A1 禁哨兵值双义 + C6 降级必须可分辨)。
64
75
  *
@@ -64,6 +64,20 @@ function assertNeverArm(_ev) {
64
64
  */
65
65
  export function eventToSdkMessage(ev, ctx) {
66
66
  switch (ev.type) {
67
+ // ── `human_input`(core 5.14.0 design/171 / server 7.4.0 SSE,黑板 [3017]/[3020])────────────
68
+ // 🔴 **到期复核已兑现(sdk 6.9.0 提货,2026-08-08)**:本臂此前是 switch **之前**的一条 raw
69
+ // 预分派(理由 = 它还没进 SDK 6.3.0 的 `AgentEvent` union,`case 'human_input'` 在 `ev.type`
70
+ // 上是编译错),头注写着「在 union 里的那天就把它搬进 switch」。sdk 6.9.0 的 events.d.ts
71
+ // 已声明该臂 ⇒ 按原定计划搬进来。这不是可选的整理:预分派用的是 `(ev as {type?:unknown})`
72
+ // 形,**不收窄** `ev`,所以臂一进 union,switch 的 `default` 就还看得见它 ⇒ B5 穷举断言
73
+ // (assertNeverArm)编译期真红。搬进来后本函数重回「每一条臂都受编译期穷举保护」。
74
+ // 处置不变:server 侧对本帧的投影明写「**本帧不带正文**(core 的臂里就没有 text 字段),所以
75
+ // 没有内容脱敏面」。因此它是**生命周期账本帧**,不是转录物 —— 投成中性内部臂交给 chrome 面
76
+ // 记账,绝不铸 transcript 行(铸一行就是替引擎编一条它没说过的话)。
77
+ // `humanInputProjection` 仍按 raw 记录读(逐键防御 + 白名单),不改口径:wire 是 JSON,引擎可能
78
+ // 比本包类型新一版,类型收窄不构成「字段一定合法」的保证。
79
+ case 'human_input':
80
+ return humanInputProjection(ev, ctx);
67
81
  // service 1.75 — `workflow_complete`: an out-of-band background-workflow completion push, ridden onto
68
82
  // the session's next stream open (BEFORE the run's own events). Projects to a NEUTRAL internal arm;
69
83
  // the upstream bridge turns it into CC's `<task-notification>` injection.
@@ -178,20 +192,47 @@ export function eventToSdkMessage(ev, ctx) {
178
192
  : {}),
179
193
  })));
180
194
  }
181
- // §E3/§E10 — BrainStatus liveness (events.d.ts:44/:80 rate_limited/retrying/reconnecting/
182
- // circuit_open). LIVE-only + EPHEMERAL (never persisted/replayed). Project to a NEUTRAL internal
183
- // `retry_status` arm forwarding the provider-neutral fields verbatim (phase/detail/retryInSec)
195
+ // §E3/§E10 — BrainStatus liveness. Phases: rate_limited/retrying/reconnecting/circuit_open +
196
+ // (core 5.x) recovered/gave_up the two TERMINAL phases; `recovered` means the retry SUCCEEDED,
197
+ // so the overlay must be TORN DOWN, not rendered as an error (see retryStatus.ts). Project to a
198
+ // NEUTRAL internal `retry_status` arm forwarding the provider-neutral fields verbatim —
184
199
  // NOT a renderable CC SDKMessage. The upstream bridge (sema layer) maps phase→SpinnerRetryStatus +
185
200
  // publishes to the sema retry-status store the spinner override reads (187's per-agent
186
201
  // `afa(agentId).retryStatus` app-state analog, ui-modules/0157_036_ui_Klo.js:31). The seam stays a
187
202
  // dumb forwarder: the provider-neutral→UI-shape mapping is the renderer layer's job (retryStatusStore).
188
- case 'status':
203
+ case 'status': {
204
+ // 🔴 开集 + 防御读(#3004 跟修批 2026-08-08,与上面 `tool_end.errorCode` 同族姿势):core
205
+ // `BrainStatus` 自 5.x 起还带 attempt / maxRetries / retryInMs 三键,server 两腿的
206
+ // `brainStatusEventData`(dist/trace/project.js)照发,而 SDK 的 `status` 臂类型只声明
207
+ // phase/detail/retryInSec(SDK 面尚未跟上 core)。类型面缺席不等于 wire 上缺席 —— 按
208
+ // unknown 读、按 number 窄化后原样透传,否则引擎真发的量在这一层就被剥掉了。
209
+ // 键集真源 = retryStatus.ts 的 `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对账)。
210
+ const st = ev;
211
+ const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
212
+ const attempt = num(st.attempt);
213
+ const maxRetries = num(st.maxRetries);
214
+ const retryInMs = num(st.retryInMs);
189
215
  return projected(stamp(ctx, armBody({
190
216
  type: 'retry_status',
191
217
  phase: ev.phase,
192
218
  ...(ev.detail !== undefined ? { detail: ev.detail } : {}),
193
219
  ...(ev.retryInSec !== undefined ? { retryInSec: ev.retryInSec } : {}),
220
+ ...(retryInMs !== undefined ? { retryInMs } : {}),
221
+ ...(attempt !== undefined ? { attempt } : {}),
222
+ ...(maxRetries !== undefined ? { maxRetries } : {}),
223
+ // 🔴 §E2 lane 身份必须透传(2026-08-08 codex 二轮复审命中的**跨 lane 状态破坏**)。
224
+ // `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
225
+ // `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
226
+ // 把两键都剥掉了。后果:`arms.ts` 的 `retryStatusArm` 首行那条子代断闸
227
+ // (`if (typeof m.parentToolCallId === 'string') return`,#47 矩阵 #5)在 wire 路径上**恒不命中**,
228
+ // 子代的 status 一直骑着主 spinner。旧代际下这只是显示噪声;`recovered` 相落地后它升级成
229
+ // 真状态破坏:**子代**的 recovered 会去 `takeRetryOverlay()` 消费掉**宿主**仍在重试的覆盖层,
230
+ // 向用户谎报「宿主已恢复」,而宿主随后真正的 recovered 反被判成「没有覆盖层」而沉默。
231
+ // 两键防御读(旧 server 缺席 ⇒ 键不 stamp,形状不变)。
232
+ ...(typeof ev.eventId === 'string' ? { eventId: ev.eventId } : {}),
233
+ ...(typeof ev.parentToolCallId === 'string' ? { parentToolCallId: ev.parentToolCallId } : {}),
194
234
  })));
235
+ }
195
236
  // MF-10 (design/99, events.d.ts task_progress) — a SUBAGENT PROGRESS TICK: the delegated sub-run's
196
237
  // CUMULATIVE usage at each turn boundary (LIVE/EPHEMERAL, never persisted/replayed). Project to a
197
238
  // NEUTRAL internal `task_progress` arm — NOT a renderable CC SDKMessage. The upstream bridge binds it
@@ -395,7 +436,44 @@ export function eventToSdkMessage(ev, ctx) {
395
436
  // 臂投)——本切片不投影。⚠️ 「压缩失败让用户看见」是 chrome/HUD 面的活,今天两端都还没接
396
437
  // (adapt 臂表同样无此臂),缺口记 0.13.0 候选,不许由本切片顺手编一个假 transcript 形。
397
438
  case 'compaction_outcome':
439
+ // ── sdk 6.9.0 提货批(2026-08-08)新入 union 的四条臂 ─────────────────────────────────────
440
+ // `wiring_manifest`:引擎的接线自述(leg/ask/tools 指纹)—— 可观测面,CC transcript 无对位物。
441
+ case 'wiring_manifest':
442
+ // `tool_approval` / `tool_approval_complete`:HITL 审批帧。**#185a 起它们是 `AgentEvent` 的臂**
443
+ // (此前只是 named SSE frame),但本切片不是它们的消费口 —— 它们由
444
+ // `hitl/toolApprovalWire.isToolApprovalFrame` + `hitl/frameRouter` 走审批卡那条链。在这里投一
445
+ // 条 transcript 行只会让同一只 ask 出现两次;`nothing('not_in_slice')` 是如实陈述,不是丢帧。
446
+ // ⚠️ 它们**必须**出现在这个 switch 里(而不是靠 default 兜):进 switch 才受 B5 编译期穷举
447
+ // 保护,才能在下一次 union 变形时把「谁不投影」这件事重新摆到人眼前。
448
+ case 'tool_approval':
449
+ case 'tool_approval_complete':
398
450
  return nothing('not_in_slice');
451
+ /**
452
+ * `approval_request`(design/172 流内审批协议,server ≥7.3.0、7.5.0 起默认开)。
453
+ *
454
+ * 🔴 与上面两条**不同源**,所以单列(2026-08-08 codex 复审第 4 轮命中:首版把三条并成一句
455
+ * 「都由 frameRouter 消费」—— 而 `isToolApprovalFrame` 只认 tool_approval/tool_approval_complete,
456
+ * 这条帧在**包内今天零消费口**,那句话是假的;pure 门 058⑬ 段把这个事实钉住了)。
457
+ *
458
+ * 🔴 分类 = `dropped/unsupported_arm` 而不是 `none/not_in_slice`(第 5 轮命中):`none` 是**静默**的,
459
+ * 只有 `dropped` 会经 `reportDroppedFrame` 走宿主 sink + console 留痕。本帧带着用户可见的决策内容
460
+ * (风险两轴 irreversible/egress/requiresRealApproval、窗口倒计时、`askId` 的持久 CAS 决策语义),
461
+ * 却没人接 —— 把这种缺口做成静默 none 就是本仓 P0 共性病那个形。宁可每次都吼一行。
462
+ * (本批之前它落 `default:` 的 `dropped/unknown_arm`,也是有痕的;#185a 把它加进 union 之后必须进
463
+ * switch 才编译得过,那一步不该顺手把痕迹弄丢。)
464
+ *
465
+ * 📋 缺口如实记账(**不**在本批修 —— 那是一条新卡链,是功能不是修复):
466
+ * · 存量路径今天仍能决断:SDK 头注(tool-approvals.d.ts)载明开关打开时同一只 ask 出两帧、顺序
467
+ * 钉死「先 legacy `tool_approval`、后 `approval_request`」,两帧同带 `approvalId`,legacy 腿在。
468
+ * · 但**降级路径不保证**:legacy respond 是 live-only + same-replica(错副本 404),而重连时
469
+ * pending 卡是靠 `approval_request` preamble 对账的 —— 「收到 legacy 帧后断线、按 Last-Event-ID
470
+ * 重连只再看到 approval_request」以及非粘性多副本部署这两条路上,今天只能等窗口到期→park/deny。
471
+ * · 接法(留给专门的一车):`isApprovalRequestFrameV1` 校形 → 按 approvalId/askId 去重并处理重连
472
+ * preamble → 带幂等键的 `decideAsk` 持久 CAS → 仅在能力位关闭/新帧不可用时回落 legacy respond。
473
+ * 在那条链落地之前,**不许**由本切片顺手编一个假 transcript 形来假装接上了。
474
+ */
475
+ case 'approval_request':
476
+ return dropped('unsupported_arm', 'approval_request');
399
477
  default:
400
478
  // B5 穷举:SDK union 加成员必须在**编译期**打红这一行(而不是让新臂在用户面静默丢帧)。
401
479
  assertNeverArm(ev);
@@ -404,6 +482,53 @@ export function eventToSdkMessage(ev, ctx) {
404
482
  return dropped('unknown_arm', String(ev.type ?? 'unknown'));
405
483
  }
406
484
  }
485
+ /**
486
+ * `human_input` → 中性内部账本臂(见 `eventToSdkMessage` 顶部的 raw 预分派注释)。
487
+ *
488
+ * 白名单逐键挑(禁 spread),与 server 侧 `humanInputEventData` 的投影面同源:
489
+ * · `inputId` / `sessionSeq` / `carrier` / `source` / `delivery` / `entryId` = 引擎铸的标识与枚举
490
+ * 值,非用户内容 ⇒ verbatim;
491
+ * · `issuer` / `actor.id` / `actor.issuer` = 宿主从入口凭据派生的**身份串**(自由文本形)——
492
+ * server 已 redact 过再上帧,本层只透传,不再二次加工(壳侧 UNTRUSTED-for-display);
493
+ * · `actor.hostAsserted` = 消费端**唯一**能判「这个署名可信吗」的位(core 的 `[from …]` 渲染靠它
494
+ * 决定加不加 `(unverified)`)—— 剥掉它比剥掉 id 更坏:剩下一个看不出真假的名字。
495
+ * · `principal` = **刻意不投影**(与 ToolApprovalFrame 的 principal 同裁:帧本就经 owner-scoped
496
+ * 流投递,租户身份不必由帧自述)。
497
+ * `carrier` 缺席 ⇒ `malformed`:一条既没有载体也没有正文的账本行,渲出去只是一行「有人输入了什么」
498
+ * 的噪声,而 dropped 至少会留痕。
499
+ */
500
+ function humanInputProjection(ev, ctx) {
501
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
502
+ const carrier = str(ev.carrier);
503
+ if (carrier === undefined)
504
+ return dropped('malformed', 'human_input');
505
+ const rawActor = typeof ev.actor === 'object' && ev.actor !== null && !Array.isArray(ev.actor)
506
+ ? ev.actor
507
+ : undefined;
508
+ const actor = rawActor === undefined
509
+ ? undefined
510
+ : {
511
+ ...(str(rawActor.id) !== undefined ? { id: str(rawActor.id) } : {}),
512
+ ...(typeof rawActor.hostAsserted === 'boolean' ? { hostAsserted: rawActor.hostAsserted } : {}),
513
+ ...(str(rawActor.issuer) !== undefined ? { issuer: str(rawActor.issuer) } : {}),
514
+ };
515
+ return projected(stamp(ctx, armBody({
516
+ type: 'human_input',
517
+ carrier,
518
+ ...(str(ev.inputId) !== undefined ? { inputId: str(ev.inputId) } : {}),
519
+ ...(typeof ev.sessionSeq === 'number' && Number.isFinite(ev.sessionSeq) ? { sessionSeq: ev.sessionSeq } : {}),
520
+ ...(str(ev.source) !== undefined ? { source: str(ev.source) } : {}),
521
+ ...(str(ev.delivery) !== undefined ? { delivery: str(ev.delivery) } : {}),
522
+ ...(str(ev.issuer) !== undefined ? { issuer: str(ev.issuer) } : {}),
523
+ ...(actor !== undefined && Object.keys(actor).length > 0 ? { actor } : {}),
524
+ ...(str(ev.entryId) !== undefined ? { entryId: str(ev.entryId) } : {}),
525
+ // 🔴 `eventId` 必须带到底(codex 复审二轮):durable 腿按 `Last-Event-ID` 续读会**重放**同一条
526
+ // 账本帧,而本地行 id 是现铸的 —— 审计/时间线消费者若没有引擎铸的稳定键,一次人工注入会被
527
+ // 记成多次。`inputId`/`entryId` 在公开形上都是可选的,`eventId` 是那个恒在的身份位。
528
+ ...(str(ev.eventId) !== undefined ? { eventId: str(ev.eventId) } : {}),
529
+ ...(str(ev.parentToolCallId) !== undefined ? { parentToolCallId: str(ev.parentToolCallId) } : {}),
530
+ })));
531
+ }
407
532
  /**
408
533
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
409
534
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no