@sema-agent/client-core 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -49,6 +49,154 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.56.0(2026-09-05)
53
+
54
+ > 发包批阶段一(2026-09-05,主会话):`package.json` 0.55.0→0.56.0、README Version 行、本段头转日期形、`FROZEN` pending 行同批;内容由 1.0.100 提货车 B 在隔离树交付,主会话收货补包侧门与 eventId 修钉(见「门与收货修钉」)。
55
+
56
+
57
+ ### 新增 —— `wiring_manifest` 帧的两段用户面事实(L-70 / L-108②③;core #524 + core 147③,server ≥7.58.0)
58
+
59
+ `wiring_manifest` 此前与两条 HITL 帧并列走 `nothing('not_in_slice')`。那句话对整份 manifest 的
60
+ **其余每一段**今天仍然成立 —— 本批只把两段**终端用户面**的事实取出来:
61
+
62
+ - `modelGate?: { class, removed[], restore }` —— 「本 run 被模型门卸掉了哪些工具、怎么恢复」。
63
+ **仅真有门卸时在场**(core 只在卸的时候铸段),缺席 = 没卸。
64
+ - `autoMode?: { armed, reason }` —— 「auto 到底武没武装、为什么」。effective 腿恒在,缺席 =
65
+ 老 mint / 外部 derive **没报**。
66
+
67
+ 新公面:
68
+
69
+ - `wiringManifestSupersetBody(ev)`(纯函数)+ `WiringManifestModelGate` / `WiringManifestAutoMode`
70
+ 两个类型;
71
+ - 内部臂 `wiring_manifest`(`INTERNAL_SDK_ARM_TYPES` 加员,载两个 `_sema_` 超集键);
72
+ - chrome 臂 `wiring_manifest`(`WiringManifestChromeEvent`,`CHROME_ARM_TABLE` 加员,`required:false`)。
73
+
74
+ ### 门与收货修钉(主会话收货,2026-09-05)
75
+
76
+ - 新门 `scripts/run-wiring-manifest-projection-test.mjs`(36 判据;gates-manifest / README Guards 同批登记):
77
+ 施工车只在壳侧写了门,包侧两发变异(`removed: []` 当真读数 / 子流断闸删除)在当时的 `npm test` 下
78
+ **全绿逃逸** —— 包是三端公共抽象,判据必须在包里。两发变异对新门当场红,复原绿。
79
+ - 修钉:`wiring_manifest` 投影臂原来**没带 `eventId`**,适配层 `wiringManifestArm` 那句透传永远是空
80
+ (新门 A1c 红先抓出)。现与别的臂同律:wire 带就上行、缺席键不在场。
81
+
82
+ ### 🔴 三条读法(写在臂注里,是消费端的硬约束)
83
+
84
+ 1. **帧是 FLAT 的**:读 `ev.modelGate` / `ev.autoMode`,**不是** `ev.manifest.*`。6.7.0 契约声明过的
85
+ 那层 `manifest:{...}` 嵌套 wire 上从未有过(SDK `events.d.ts` 该臂头注 2026-08-07 已按真 capture 更正);
86
+ server 把 core `TaskEvent` 的 `ev.manifest` 过白名单 builder 后 **spread 进帧**,三条腿
87
+ (live SSE / durable 账本行 / resume 重放)同源。
88
+ 2. **缺席不可反推**:两段缺席都**不许**渲成肯定句(「没有工具被卸掉」/「auto 未武装」)。
89
+ 两段都不成形时本臂**根本不铸**(不产空臂)—— 所以宿主见到本臂就至少有一段是真读数。
90
+ 3. **`autoMode.reason` 六词逐字呈现,绝不映射** `/v1/capabilities.permissionModeAuto.reason` 的六词:
91
+ 两套词表**同名不同义**(`settings_denied` 在 capabilities 那边折 `no_intent` 不折 `denied`),
92
+ 归一会造出一个假读数。`modelGate.restore` 同理原样呈现 —— 宿主自己拼一句恢复办法,
93
+ 等于替引擎编了一条它没说过的出口。
94
+
95
+ ### 形校验(两段各自独立,一段坏不牵连另一段)
96
+
97
+ - `modelGate`:`class` / `restore` 非空串 **且** `removed` 是全串数组 **且** 过滤空串后**非空**,
98
+ 三件缺一整段不收(不折 `''`、不折空数组、不编默认恢复办法)。空 `removed` 与「没卸」在屏面上
99
+ 不可区分,而 core 的契约是「有门卸才铸段」⇒ 它是坏值不是真读数。
100
+ - `autoMode`:`armed` 是 boolean **且** `reason` 非空串 **且** 满足 server 侧同一条互证式
101
+ `armed === (reason === 'armed')`。照抄这条不是「重复上游」——**非投影口**(宿主自建管线 /
102
+ 重放存量转录)喂进来的帧不过 server,一个 `{armed:true, reason:'settings_denied'}` 会让消费端
103
+ 同时读到「武装了」和「被拒了」。
104
+ - 子流断闸(带 `parentToolCallId` 的自述不上 leader 面)按「**键在不在**」判、不按「是不是串」判 ——
105
+ 与 `text_segment_end` 同因:本臂决定的是「这份自述算谁的」,一个坏值会把子代的门卸清单
106
+ 擦成 leader 的,用户会以为**自己这一 turn**少了工具。
107
+
108
+ ### 已知局限(本版新增)
109
+
110
+ - **超集键名是临时形**:`modelGate` / `autoMode` 今天**不在**已发布 SDK(8.1.0)的 `AgentEvent`
111
+ wiring_manifest 臂里(全树零命中,亲验)⇒ 本包用 `_sema_` 前缀而不自铸 SDK 同名形。SDK 声明该臂
112
+ 之后改名是 **BREAKING**,另立一批。投影层那行 `as unknown as Record<string, unknown>` 就是
113
+ 「上游类型还没到货」的可见落点,删掉它编译器会立刻指出所有该跟着改的地方。
114
+ - **整份 manifest 仍不投影**:leg / ask / question / interaction / elicit / parkLane / session /
115
+ fleet / permissionRules / governance / configFingerprint 十一段一件没接。想要它们的宿主今天
116
+ 只能走 operator 面的 `GET /v1/diagnostics/wiring`(且那面 403 门另有前提)。
117
+
118
+ ## 0.55.0(2026-09-05)
119
+
120
+ ### `waiting_first_token` —— 等第一个 token 是一种**状态**,不是一次错误(core 7.3.0 / #530;cli B-003 / L-108①)
121
+
122
+ 用户报案(sema-bug4「模型调用失败对用户不可见」)的两个面,同一个根因:
123
+
124
+ | 引擎 | 用户看到 |
125
+ |---|---|
126
+ | 7.57.0(内嵌 core 7.2.0) | **什么都没有** —— 相不存在,壳无从渲 |
127
+ | 7.58.0(内嵌 core 7.3.1) | 一条**错误色**的 `API error` 行 —— 方向反了:一次尝试都没失败过 |
128
+
129
+ core 7.3.0 在 `BrainStatusPhase` 上加了第七相 `waiting_first_token`:首 token 看门狗已武装
130
+ (`firstTokenTimeoutMs > 30_000`)而第一个字还没到,等满 30s 起每 30s 发一帧,携 `elapsedMs` /
131
+ `timeoutMs`(`core/dist/brain/stream-engine.js` 的 `emitWaitFrame` 铸点)。本包 0.54.0 及以前
132
+ `mapBrainStatusToRetry` 的 `switch` 只认六相 ⇒ **第七相落 `default:` 被投成 `{kind:'error'}`**,
133
+ 壳照 `kind` 渲成重试行。病形 = 「上游闭集加员落 default 臂 ⇒ 新状态被渲成最像的旧状态」,
134
+ 与 #3004 修的 `recovered`(重试**成功**被渲成 `API error`)逐字同族。
135
+
136
+ - **新 `kind: 'waiting'` 臂**(`src/retryStatus.ts`):携 `elapsedMs` / `timeoutMs`,不打 `terminal`
137
+ 位(首 token 到了 core 补 `recovered`,彻底失败补 `gave_up` —— 等待本身从来不是终态)。
138
+ `deadline` 恒 = 映射时刻的 `nowMs`(本相不带 `retryIn*`)⇒ 渲染面按「剩余量 ≤ 0」自然不渲倒计时。
139
+ - 🔴 **老宿主分两形(不许含混说「零差异」)**:`waiting` 臂刻意是 `error` 臂的**结构超集** ——
140
+ **结构形**消费者(`if (kind==='stalled') … else …`)逐字节零差异;而**穷尽形**消费者
141
+ (`switch (kind)` + `default: assertNever`)**加员即破**(重新编译报「waiting 不可赋给 never」,
142
+ 已编译老产物在 default 臂**抛**)—— 破的是判别值本身,补结构位救不了。⇒ 对这一类消费方
143
+ 0.55.0 是 **BREAKING**,三端必须审一遍自己的 `RetryStatus` switch。两形的事实分别钉在
144
+ `run-client-core-pure-test.mjs` G6-e ①②③。结构形的细节:还没跟车的端
145
+ (desktop / web-client)其 `if (kind==='stalled') … else …` 的 else 臂照旧读到 `error` /
146
+ `deadline` / `terminal`,渲出与 0.54.0 **逐字节相同**的那一行。少了 `error` 这一位,
147
+ `status.error.rateLimits` 就是一次 TypeError —— 渲染路径裸抛 = 整屏崩,把「一行渲错」升级成
148
+ 「一屏全没」。判据不是一句承诺,是门:`run-client-core-pure-test.mjs` G6-d 与
149
+ `run-additive-key-passthrough-test.mjs` G5d 拿**本函数自己的 `default:` 臂**当活对照
150
+ (一个未知相走的正是加员前第七相走的那条臂),两边逐位相等才绿。
151
+ - **两位跟车过两层白名单**:`BrainStatus` 加 `elapsedMs` / `timeoutMs`(九键 → **十一键**)。
152
+ 🔴 族扫落点是**两处**,不是派工单点名的那一处:`adapter/downstream/eventToSdkMessage.ts`
153
+ 的 `case 'status'`(wire → 内部臂)与 `adapt/arms.ts` 的 `retryStatusArm`(内部臂 → chrome 事件)。
154
+ 漏任一层,新相到壳只剩空壳:kind 对了、秒数永远缺席。两位缺席一律**键不 stamp**,绝不折 0。
155
+ - **相位运行期镜像**六相 → **七相**;devDep `@sema-agent/core` `^7.1.0` → `~7.3.0`
156
+ (对账物不抬,`engine-vocab` G2-b 对新相**没有判别力** —— 这正是该门头注记过五次的恒绿病根)。
157
+
158
+ ### 抬对账物同批捞出的三件存量(族扫,不是点名件)
159
+
160
+ 抬 devDep core 到 7.3.1 后 `engine-vocab` 门当场翻红三处,逐条如实修:
161
+
162
+ 1. **`CC_DETAIL_TYPES` 漏 `list-agents`**(42 / core 43)—— ListAgents 的结构化卡
163
+ (`core/dist/agents/list-agents-tool.js` 铸点)。漏词的实际后果是那一类卡**永远退回正则解
164
+ 模型面文本**,而任何一层都不会响。与 5.20 / 5.43 两次逐字同族。
165
+ 2. **`EditedRuleTextPrecheck` ok 臂漏 `warnings`**(core 7.3.0 / design/382 §3.3-5 的**宽度提示**)。
166
+ 这条腿是端口注入(值原样穿过),所以漏的不是运行期的值,是**消费端的类型面可见性** ——
167
+ 端读不到就不会渲,「宽度在点头之前被说出来」就只剩引擎那半边。同批转出
168
+ `EditedRuleBreadthWarning`(`code` 按本包一贯纪律取**开集串**,不镜像 core 的闭集枚举)。
169
+ 3. **门自身的抽取口径病**:`extractStringUnion` 的 `([^;]+);` 在**第一个分号**处截断,而 core 7.3.0
170
+ 的 `BrainStatusPhase` 成员之间夹着一段**正文带分号**的 JSDoc ⇒ 第七相被静默漏抽,门报出
171
+ 「core 6 相 / 本包 7 相;多:waiting_first_token」这种**方向反了的**红 —— 一个抽取口径的病
172
+ 足以把跟车做对的人判成做错。修 = 先剥注释再匹配(同族的 `extractInterfaceKeys` 早就这么做),
173
+ 并补两条自证腿(带分号 JSDoc 夹层 / 剥注释后不把注释里的串当成员)。
174
+
175
+ ### 已知局限(本版新增)
176
+
177
+ - 壳半场的文案是 sema 超集:CC 2.1.250 语料**无同形提示行**(`cli250.js` 里 `first token` /
178
+ `firstToken` / `first_token` 的全部命中都是账号首 token 日期与流式计时**遥测**,无渲染面;
179
+ 同族里唯一的用户可见行是 `Waiting for API response`,那是 `reconnecting` 相的既有行)。
180
+ ⇒ 措辞按 [6386] / L-79② 既定形,登记 divergence。
181
+ - 未跟车的**结构形**端(desktop / web-client)在 7.58.0 引擎上仍会渲 0.54.0 那一行。这是加员的
182
+ **设计选择**而不是遗漏:零崩渲、零行为漂移,跟车与否由各端自己排期。**穷尽形**端不在此列 ——
183
+ 见上一条,那是 BREAKING,必须加 case。
184
+ - 🔴 **本版发出去也还不能让用户看见这一行**(异源对抗复审 [high] 如实记):cli 侧 deps 仍是
185
+ `@sema-agent/client-core ^0.53.0` / `@sema-agent/core ^7.2.0`,ENGINE_PIN 仍是 server 7.57.0
186
+ (内嵌 core 7.2.0,**没有**这个相)。⇒ 端到端可见需要三件同批:0.55.0 发布 → cli deps 抬到
187
+ ^0.55.0(含 lockfile)→ ENGINE_PIN 抬到 ≥7.58.0,并在**那一对真版本**上重跑 live 腿、
188
+ 摘掉它在 `skip-baseline.json` 里的必 SKIP 登记。
189
+
190
+ - **发包前异源扫描四修(2026-09-05,三域 opus 扫描 + opus 反证,17 条 7 实证)**:①[medium] **waiting 相的覆盖层拆除生命周期无门**——
191
+ 把 `retryStatusArm` 尾行 `noteRetryOverlay()` 对 waiting 摘掉,pure 3455 + additive 81 照样全绿 ⇒ 补 R1w 三判据(waiting 后首个主 lane
192
+ 内容帧即 `status:null`、先于 `response_metrics start`、子流帧不清);②[low] `elapsedMs`/`timeoutMs` 负数/NaN 直穿 ⇒ `saneMs` 只认有限非负
193
+ 数(缺席不渲 0);③[medium] cli 侧 waiting 相曾走 RetryStatusRow 的 all-or-nothing 槽,整块顶掉动画 glyph 与每秒计时,30s 一帧之间屏上零变化
194
+ (像卡死)⇒ 壳改为只替换 byline、glyph 照常动(cli 半场);④文档/门勘误:`retryStatus.ts` 键集注释 9→11 键、server 7.54.0→7.58.0;typeshape
195
+ 棘轮 b4 21→20 的推导理由改按实测(此前只有一处内联);engine-vocab `CORE_FLOOR` 7.1→7.3 与对账物同步;INTEGRATION-CLIENTS §0a 已发版本的
196
+ 「未发」告示按「发出即退场」约定退场。**老宿主三形**(§18c 更正):结构形(读 `kind` 走 default)零差异;穷尽形(`switch`+`assertNever`)
197
+ BREAKING;**不读 kind 的宿主**(web-client 现状)在 waiting 相上仍渲既有错误行「Request failed · retrying」——不是回归(0.54.0 同形),
198
+ 但接 §18b 才有真进度行。
199
+
52
200
  ## 0.54.0(2026-09-05)
53
201
 
54
202
  ### design/385 —— 三条引擎注入车道的类型化投影(`src/peerFrames.ts`;cli L-61② / L-87 5a①5a②)
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.54.0
38
+ **Version:** 0.56.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -264,6 +264,7 @@ public-surface guard checks that last one).
264
264
  | `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible |
265
265
  | `scripts/run-esc-halt-plan-test.mjs` | The Esc stop decision every client shares: fire the **turn-level** halt first, and escalate to a **run-level** cancel in exactly two cases — the engine itself answered with a 409 from the closed code set (it is saying "there is no in-flight turn here; use cancel for a run-level stop"), or that shot came back with no verdict at all *and* the shell can independently prove a permission card was on screen. Everything else does not escalate. The asymmetry is the whole point and every negative control guards the same direction — deciding *not* to escalate costs the user one more choice on a busy-session card (recoverable), deciding to escalate wrongly tears down a run that was alive and takes every in-flight tool with it (not). So: the closed code set is a **frozen** value, not a `ReadonlySet` — type-level immutability does not stop a consumer's `.add()`, and the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift; the escalation gate is the **conjunction** of that closed set and the 409 status, since honouring the code alone lets a 500 that merely quotes it drive a destructive call; `interrupt.not_held` and `steering.not_running` are deliberately outside the set (the first means *this replica* has no live face — the run may be perfectly alive on another); an unreadable code falls to the no-escalation side; a `parked` flag never overrides a verdict the engine did give, and only strict `true` counts when it did not. The first shot is unconditional by construction — it does not consult `parked`, because the 409 it earns is exactly the verdict the gate wants — and the verdict itself is a closed machine-readable reason word, not display copy |
266
266
  | `scripts/run-peer-frame-projection-test.mjs` | The three engine-injected lanes design/385 puts on the **one** `task_notification` carrier, which are not the same kind of thing at all: a delegated child's uplink (`agentMessage`), another session's message drained from this session's own box (`crossSessionMessage`), and a receipt about one of *this* session's own outbound messages (`crossSessionNotice`). The engine renders none of them inside a `<task-notification>` shell, so a client that projects them as the generic completion card shows "background task finished" while the model read a colleague's sentence — two faces describing different events. The discriminator is pinned to the **typed carrier being present**, never to the `summary` text: those carriers can only be minted by the engine's injection legs (the external `notify()` input is a strict subset of the payload and can wear none of them), while `summary` is filled by every notification there is — so anchoring on text would let any background task impersonate a colleague's message by writing `<agent-message from="…">` into its own summary, and a positive control asserts exactly that payload still projects as the generic card. Fail-closed has two tiers rather than one: a broken **required** field (empty `from`, a non-string `body`, a notice `kind` outside the closed set) returns absence so the caller falls back to the generic card — an honest downgrade where the user still sees the notification — while a broken **optional** field drops only itself, because losing an attribution note and losing a colleague's whole message are not the same magnitude. The provenance side record is **required and must agree on four points** (`kind` matches the lane; `from`/`taskId`/`seq` are present and equal the carrier/payload — each equality is anchored on a core mint site and pinned by the cli wire-anchor A-K24), so a carrier signed with a trusted name but a disagreeing provenance falls back to the generic card; peer bodies pass the same authority-envelope neutralization core applies (`<task-notification>` etc. are defused) so a colleague's text can never seed the resume dedup ledger. Lane precedence copies the engine renderer's own order, because the model already read the frame in that order and a client ordering of its own would put a card on screen that disagrees with the frame the model saw. Rendering and parsing of the transcript line live in the same module and are round-tripped in both directions, including a body carrying a forged closing tag (a parser fooled there hands half a message to the next row) and a quote inside the sender label (which must not forge a second attribute); the notice lane is deliberately kept **out** of the parser, since recognising it would mean anchoring the `[Cross-session …]` prefix and a user typing that same line would be rendered as engine speech. Hostile carriers are read as own **data** descriptors only and accessors are never invoked at all — `catch` catches throwing, not never returning — proven by a counting getter that must stay at zero calls, alongside a revoked proxy and a prototype-only carrier; and four legacy payload shapes assert the no-carrier path is byte-identical to before, which is the executable form of "zero difference for an older host" |
267
+ | `scripts/run-wiring-manifest-projection-test.mjs` | The two end-user facts carried on the engine's `wiring_manifest` frame (`modelGate`: which tools this run's model gate removed and the verbatim restore hint; `autoMode`: whether auto mode is actually armed and the engine's own reason word). Projection: both sections ride as `_sema_`-prefixed superset keys, verbatim, and no SDK-named key is minted; a frame where neither section is well-formed projects to `none/not_in_slice` (no empty arm); `modelGate` needs all three keys and treats `removed: []` as a bad value rather than a reading; `autoMode` needs a boolean plus a non-empty reason that agrees with it, and the reason word is never mapped onto the capabilities vocabulary; the frame is flat (a nested `manifest:{}` wrapper is not a supply); `eventId` rides like every other arm. Adapter: exactly one chrome event on the main lane, a sub-flow frame (any `parentToolCallId`, `null` included) yields nothing, and an absent `eventId` leaves the key absent. Added at receiving time because the shell-side gate could not see this package's behaviour: two mutations (empty `removed` accepted, sub-flow gate removed) had passed the package suite untouched |
267
268
 
268
269
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
269
270
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -294,6 +294,38 @@ const engineNoticeArm = function* (m) {
294
294
  ...(typeof m.eventSeq === 'string' && m.eventSeq.length > 0 ? { eventSeq: m.eventSeq } : {}),
295
295
  });
296
296
  };
297
+ /**
298
+ * `wiring_manifest` 内部臂 → chrome `wiring_manifest`(core #524 + core 147③,server ≥7.58.0)。
299
+ *
300
+ * 契约本体在 `seam.ts` 的 {@link WiringManifestChromeEvent} 头注(它是宿主要读的那一份)。
301
+ * 本臂只做两件:
302
+ * 🔴 **子流断闸**(与 `engine_notice`/`text_segment_end` 同族):带 `parentToolCallId` 的接线自述
303
+ * 属于子代/编排腿,把它的门卸清单渲到 leader 面上,用户会以为**自己这一 turn**少了工具。
304
+ * 断闸按「**键在不在**」判不按「是不是串」判(与 `textSegmentEndArm` 同因:本臂决定的是
305
+ * 「这份自述算谁的」,方向必须更严;`null` 算在场,不给坏值开口子)。
306
+ * 🔴 **第二道成形判**:投影层已把两段各自校验过并保证「至少一段在场」,这里只再确认一次
307
+ * ——防的是**非投影口**喂进来的帧(宿主自建管线 / 重放存量转录),不是重复判据。
308
+ * 两段都不在场 ⇒ 不产事件(与投影层的「不铸空臂」同一条规矩,两层不许各说各话)。
309
+ * 🔴 **一个字都不改**:段内容原样转发(`restore` 逐字、`reason` 六词不映射)——任何加工都必须
310
+ * 发生在渲染面,理由见臂注义务②③。
311
+ */
312
+ const wiringManifestArm = function* (m) {
313
+ if (m.parentToolCallId !== undefined)
314
+ return;
315
+ const modelGate = m._sema_modelGate;
316
+ const autoMode = m._sema_autoMode;
317
+ const hasGate = typeof modelGate === 'object' && modelGate !== null;
318
+ const hasAuto = typeof autoMode === 'object' && autoMode !== null;
319
+ if (!hasGate && !hasAuto)
320
+ return;
321
+ yield chrome({
322
+ kind: 'wiring_manifest',
323
+ laneProof: MAIN,
324
+ ...(hasGate ? { modelGate: modelGate } : {}),
325
+ ...(hasAuto ? { autoMode: autoMode } : {}),
326
+ ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
327
+ });
328
+ };
297
329
  /**
298
330
  * `text_end` 内部臂 → chrome `text_segment_end`(#323 / core #447)。
299
331
  *
@@ -765,7 +797,7 @@ const retryStatusArm = function* (m, { ctx, flags }) {
765
797
  // #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
766
798
  if (typeof m.parentToolCallId === 'string')
767
799
  return;
768
- // 🔴 六键全转(#3004 跟修批 2026-08-08):此前这里只挑 phase/detail/retryInSec 三键,attempt /
800
+ // 🔴 十一键全转(0.55.0;首建 #3004 跟修批 2026-08-08):此前这里只挑 phase/detail/retryInSec 三键,attempt /
769
801
  // maxRetries / retryInMs 被剥在本层 —— 而 server 两腿的 brainStatusEventData 是全发的,
770
802
  // `RetryStatus` 上也早就留好了 attempt/maxRetries 两位。键集真源 =
771
803
  // `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对实装 core 对账)。
@@ -789,6 +821,14 @@ const retryStatusArm = function* (m, { ctx, flags }) {
789
821
  // 剥掉它 = 用户看得到「在重试」却看不到「谁失败了/该不该等」)。
790
822
  ...(typeof m.retryAtMs === 'number' ? { retryAtMs: m.retryAtMs } : {}),
791
823
  ...(typeof m.errorStatus === 'number' ? { errorStatus: m.errorStatus } : {}),
824
+ // ── 十一键(0.55.0;core 7.3.0 #530,server ≥7.58 `brainStatusEventData` 放行)──────────
825
+ // 🔴 **同一个病形的第五、第六例**,而且这一次剥掉的是**新相的全部内容**:
826
+ // `waiting_first_token` 帧上除了 phase 就只有这两位携带信息(它不带 retryIn*/attempt/
827
+ // errClass/errorStatus —— core 明说本相不宣告等待)。这层白名单不跟车,`mapBrainStatusToRetry`
828
+ // 的新 `waiting` 臂就只能收到一个空壳:kind 对了、秒数永远缺席,屏上那行渲不出「等了多久」。
829
+ // ⇒ 加相**必同批加键**;键集真源仍是实装 core 的 `BrainStatus` 整个键集(G2-c 逐键等值门)。
830
+ ...(typeof m.elapsedMs === 'number' ? { elapsedMs: m.elapsedMs } : {}),
831
+ ...(typeof m.timeoutMs === 'number' ? { timeoutMs: m.timeoutMs } : {}),
792
832
  }, ctx.now());
793
833
  // `status === null` = 引擎直报 `recovered`(重试成功)。这一拍是**清除腿之一** —— 但间接腿
794
834
  // (TurnFlags 的「主 lane 内容帧到达即清」)可能已经清过了:已安装 core 在内容流跑完的 finally
@@ -825,6 +865,7 @@ export const ARMS = new Map([
825
865
  ['human_input', humanInputArm],
826
866
  ['engine_notice', engineNoticeArm],
827
867
  ['text_end', textSegmentEndArm],
868
+ ['wiring_manifest', wiringManifestArm],
828
869
  ['prompt_suggestions', promptSuggestionsArm],
829
870
  ['retry_status', retryStatusArm],
830
871
  ]);
@@ -116,6 +116,60 @@ export type EventProjection = {
116
116
  * 旧写法 `if (msg)` 在新返回型上恒真(对象永远 truthy),所以这是**必须点名**的一类改动。
117
117
  */
118
118
  export declare function eventToSdkMessage(ev: AgentEvent, ctx: EmitContext): EventProjection;
119
+ /**
120
+ * `engine_notice` → 中性内部通告臂(#310 / #318 件①,契约 = server `ASSISTANT-WIRE-CONTRACT` 附录 D
121
+ * + openapi `Event_engine_notice`)。
122
+ *
123
+ * ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
124
+ * 引擎的**结构化通告**里,被 server 判为面向**本会话终端用户**的那一小撮,按 `sessionId` 路由到
125
+ * 这条会话的流上。live 与 durable **两腿都有**(server 三条 run 腿都挂了口)⇒ 断连重连的重放里
126
+ * 会**再看到它**,与 `workspace_changed` 同一条**幂等消费**纪律(附录 D.1 逐字)。
127
+ *
128
+ * ── 🔴 开集三条(本函数的全部判据,逐条都是「不许做什么」)────────────────────────────────
129
+ * ① **按 `code` + `detail` 消费,`message` 只作 fallback 展示**。core 明写
130
+ * `memory.session_polluted` 的 message 随 `memoryProvenance` 模式变文 ⇒ 按 message 文本匹配
131
+ * **必碎**(5.41 合流码形退役同教训)。所以本层把三者**分别**上臂,绝不把 detail 折进文案。
132
+ * ② **`code` 是开集,认不得也绝不丢帧**。server 的白名单会随 core 码册增长(起步三码 →
133
+ * core 5.47/5.48 的 hold 三码入册,附录 D.3 的表尚未跟上)。本层因此**一个码都不硬编**:
134
+ * 没有识别表、没有 switch、没有「已知才投」——认不认得是**渲染面**的判断,不是投影面的门。
135
+ * ⇒ 本函数对 `code` 唯一的要求是「非空串」(判别键本身缺席才叫畸形)。
136
+ * ③ **能力位不当两值门**:本臂不读任何 caps、不问「引擎支不支持 engine_notice」。帧到了就投影
137
+ * (A-022「到帧即服务」先例)—— 拿一个探测位去 gate 一条**已经到手的事实**,只会在探测未判/
138
+ * 失败时把真事实丢掉。
139
+ *
140
+ * ── 🔴 消费端纪律(写在臂上,因为三端各写一遍必漂)────────────────────────────────────────
141
+ * · `detail` 已过 server 的 `redactSecrets` + 尺寸 bound(自由文本 1000 字符;深 4 / 键 32 /
142
+ * 数组 32),**仍按外部串处理**(呈现面字符处理走消费方自己的单源)。
143
+ * · `ts` 是 **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。
144
+ * · 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
145
+ * core 顶注):两个数各自读、并列呈现,任何减法都会得出一个**不存在的量**。本层原样透传 detail
146
+ * 正是为了让这条纪律只在渲染面兑现一次,而不是被投影层先算一个差值出来。
147
+ * · **本帧的缺席不代表「没发生」**:非白名单码 / 缺 `sessionId` 的通告 server **如实不投**
148
+ * (宁缺席不串台),全族那一份始终在 server 的结构化日志里(运维面)。所以消费端**绝不许**
149
+ * 从「没收到 engine_notice」反推「记忆姿态正常」。
150
+ *
151
+ * ── 畸形判据(fail-closed 方向)────────────────────────────────────────────────────────────
152
+ * `code` 非串/空串 ⇒ `malformed`(判别键都没有的通告,渲出去只是一行没有主语的噪声,而 dropped
153
+ * 至少会经 `reportDroppedFrame` 留痕)。其余四键**各自**按诚实缺席处理:`message` 非串 ⇒ 空串
154
+ * (fallback 位缺席,渲染面据此走纯 code 呈现)、`detail` 非对象 ⇒ 空对象(**不是**丢帧:通告的
155
+ * 承重物是 code,detail 坏了不该连带把「这件事发生过」一起吞掉)、`sessionId` / `ts` 非法 ⇒ 键不
156
+ * stamp(绝不铸 `0` 这种看起来合法的假读数)。
157
+ */
158
+ /** {@link wiringManifestSupersetBody} 的 `modelGate` 段(core #524 / server S-86 的形)。 */
159
+ export interface WiringManifestModelGate {
160
+ /** 门的类别词(core 开集,原样透传;例:模型族名)。 */
161
+ class: string;
162
+ /** 本 run 被门卸掉的工具名(**非空**;空数组不是真读数,见投影函数头注)。 */
163
+ removed: readonly string[];
164
+ /** 恢复办法的**逐字**提示(core 铸,例:环境变量赋值串)。渲染面原样呈现,不要自己拼。 */
165
+ restore: string;
166
+ }
167
+ /** {@link wiringManifestSupersetBody} 的 `autoMode` 段(core 147③ 的形)。 */
168
+ export interface WiringManifestAutoMode {
169
+ armed: boolean;
170
+ /** core **六词逐字透传**。🔴 不映射 `/v1/capabilities.permissionModeAuto.reason` —— 见投影函数头注。 */
171
+ reason: string;
172
+ }
119
173
  /**
120
174
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
121
175
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
@@ -46,6 +46,10 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
46
46
  'engine_notice',
47
47
  // #323 / core #447:assistant 流式**散文段边界**(raw 预分派铸点,见 `textEndProjection` 头注)。
48
48
  'text_end',
49
+ // L-70 / L-108②③(core #524 + core 147③,server ≥7.58.0):`wiring_manifest` 帧上**两个新段**的
50
+ // 超集投影(`_sema_modelGate` / `_sema_autoMode`)。整份 manifest 的其余段仍不在本切片里 ——
51
+ // 射程写在 `case 'wiring_manifest'` 头注,别读成「manifest 接上了」。
52
+ 'wiring_manifest',
49
53
  ]);
50
54
  /** Wrap neutral content blocks in the CC `assistant` message envelope. */
51
55
  function assistantArm(ctx, content) {
@@ -344,6 +348,11 @@ export function eventToSdkMessage(ev, ctx) {
344
348
  // 不等 SDK**,类型面缺席不等于 wire 上缺席。
345
349
  // 📋 **退役条件**:sdk 把这两位补进 `status` 臂声明的那一批,把它们并进上面的类型面直读、
346
350
  // 删掉本段的 `st2` 结构视图(与本批刚兑现的四键退役逐字同一条路)。
351
+ // 🔴 **0.55.0 又添两位,同一条退役路**(core 7.3.0 #530 / server ≥7.58 `brainStatusEventData`
352
+ // 放行):`elapsedMs`(第一个 token 已被等了多久)与 `timeoutMs`(这次等待的天花板)。
353
+ // 它们只在新相 `waiting_first_token` 上在场,而那一相**除了 phase 就只有这两位带信息** ——
354
+ // 本层剥掉 = 新相到了下游只剩一个空壳,屏上渲不出「等了多久」。sdk 的 `status` 臂同样尚未
355
+ // 声明这两位 ⇒ 与上面两位同款结构视图读,退役条件同(sdk 补声明的那一批一起并进类型面直读)。
347
356
  const st2 = ev;
348
357
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
349
358
  const attempt = num(ev.attempt);
@@ -351,6 +360,8 @@ export function eventToSdkMessage(ev, ctx) {
351
360
  const retryInMs = num(ev.retryInMs);
352
361
  const retryAtMs = num(st2.retryAtMs);
353
362
  const errorStatus = num(st2.errorStatus);
363
+ const elapsedMs = num(st2.elapsedMs);
364
+ const timeoutMs = num(st2.timeoutMs);
354
365
  // 非空串才透传(空串既不是桶也不是「不知道」,只会在下游被渲成一个空的原因)。
355
366
  const errClass = typeof ev.errClass === 'string' && ev.errClass.length > 0 ? ev.errClass : undefined;
356
367
  return projected(stamp(ctx, armBody({
@@ -370,6 +381,11 @@ export function eventToSdkMessage(ev, ctx) {
370
381
  // **全链**(adapt 入口 → chrome 事件),所以漏任一层它都红。
371
382
  ...(retryAtMs !== undefined ? { retryAtMs } : {}),
372
383
  ...(errorStatus !== undefined ? { errorStatus } : {}),
384
+ // ── 0.55.0 跟车两位(core 7.3.0 #530 / server ≥7.58 放行)──────────────────────────
385
+ // 族扫同址:本条腿的**两层白名单**(本层 + `adapt/arms.ts` 的 `retryStatusArm`)本批同改,
386
+ // 与上面 0.48.0 那两位逐字同一个理由;漏任一层,新相 `waiting_first_token` 的秒数就到不了壳。
387
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
388
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
373
389
  // 🔴 §E2 lane 身份必须透传(2026-08-08 对抗复审二轮复审命中的**跨 lane 状态破坏**)。
374
390
  // `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
375
391
  // `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
@@ -480,6 +496,43 @@ export function eventToSdkMessage(ev, ctx) {
480
496
  // rides on `notification`). Projects to a NEUTRAL internal arm: the REPL bridge drops it (the ENGINE
481
497
  // already steer-injects the model-facing XML), the print-mode `ask` converts it to CC's
482
498
  // `system/task_notification` SDK frame.
499
+ /**
500
+ * `wiring_manifest` —— 引擎的**接线自述**(leg / ask / question / parkLane / … 指纹)。
501
+ *
502
+ * 🔴 **本臂只投两段,不是整份 manifest**(射程写在这里,免得下一个人读成「manifest 接上了」)。
503
+ * 整份 manifest 是 operator / 诊断面的读物,CC transcript 无对位物 —— 此前本臂与两条 HITL 帧
504
+ * 并列走 `nothing('not_in_slice')`,那句话对**其余每一段**今天仍然成立。改的只是:`modelGate`
505
+ * 与 `autoMode` 这两段是**终端用户面**的事实(「这一 run 你少了哪些工具」/「auto 到底武没武装」),
506
+ * 它们没有第二条到达消费端的路,留在 not_in_slice 里就是静默蒸发。
507
+ *
508
+ * 🔴 **帧是 FLAT 的,没有 `manifest` 包层**(SDK `events.d.ts` 的 wiring_manifest 臂逐字:
509
+ * server 把 core `TaskEvent` 的 `ev.manifest` 过白名单 builder 后 **spread 进帧**,三条腿
510
+ * (live SSE / durable 账本行 / resume 重放)同源)⇒ 读 `ev.modelGate` / `ev.autoMode`,
511
+ * **不是** `ev.manifest.modelGate`。6.7.0 契约声明过的那层 `manifest:{...}` 嵌套 wire 上从未有过。
512
+ *
513
+ * 🔴 **两段各自独立**:一段畸形不牵连另一段;两段都不成形 ⇒ 仍回 `none/not_in_slice`,
514
+ * **不铸空臂** —— 每条腿都会来一帧 wiring_manifest,为它们各铸一条什么都不说的内部臂只会让
515
+ * 消费端每 run 多收一条噪声,还把「本 run 真有门卸」这件事稀释掉。
516
+ *
517
+ * 🔴 **超集键走 `_sema_` 前缀**:`modelGate`/`autoMode` 今天**不在**已发布 SDK 的 `AgentEvent`
518
+ * wiring_manifest 臂里(sdk 8.1.0 `dist/events.d.ts` 全树零命中,亲验)。本包不许自铸 SDK 同名
519
+ * 形去冒充上游词汇;等 SDK 声明该臂后再谈改名(那是 BREAKING,另立)。
520
+ */
521
+ case 'wiring_manifest': {
522
+ // 🔴 `as unknown as Record<…>` 不是图省事:sdk 8.1.0 的 wiring_manifest 臂**没有**这两段
523
+ // (亲验全树零命中),所以 `ev` 在这里被窄成一个不含它们的具名形 —— 直接读会是编译期
524
+ // 「属性不存在」。这行 cast 就是「上游类型还没到货」这件事的**可见落点**;SDK 声明该臂
525
+ // 之后把它删掉,编译器会立刻指出所有该跟着改的地方。
526
+ const raw = ev;
527
+ const body = wiringManifestSupersetBody(raw);
528
+ if (body === undefined)
529
+ return nothing('not_in_slice');
530
+ // `eventId` 与别的臂同律带上(core 铸的事件身份;wire 未必带 ⇒ 缺席时键不在场)。
531
+ // 🔴 主会话收货门 A1c 抓出的死臂:适配层 `wiringManifestArm` 读 `m.eventId`,而本层原来没投它,
532
+ // 那句透传永远是空 —— 消费端拿不到幂等键,durable 重放只能退到内容去重。
533
+ const eventId = typeof raw.eventId === 'string' && raw.eventId.length > 0 ? { eventId: raw.eventId } : {};
534
+ return projected(stamp(ctx, armBody({ type: 'wiring_manifest', ...body, ...eventId })));
535
+ }
483
536
  case 'task_notification': {
484
537
  // 载荷两形:嵌在 `notification` 里(server 早期形)或直接摊在帧上(今天的 union 形)。
485
538
  const nested = ev.notification;
@@ -633,8 +686,6 @@ export function eventToSdkMessage(ev, ctx) {
633
686
  // (adapt 臂表同样无此臂),缺口记 0.13.0 候选,不许由本切片顺手编一个假 transcript 形。
634
687
  case 'compaction_outcome':
635
688
  // ── sdk 6.9.0 提货批(2026-08-08)新入 union 的四条臂 ─────────────────────────────────────
636
- // `wiring_manifest`:引擎的接线自述(leg/ask/tools 指纹)—— 可观测面,CC transcript 无对位物。
637
- case 'wiring_manifest':
638
689
  // `tool_approval` / `tool_approval_complete`:HITL 审批帧。**#185a 起它们是 `AgentEvent` 的臂**
639
690
  // (此前只是 named SSE frame),但本切片不是它们的消费口 —— 它们由
640
691
  // `hitl/toolApprovalWire.isToolApprovalFrame` + `hitl/frameRouter` 走审批卡那条链。在这里投一
@@ -761,44 +812,80 @@ function textEndProjection(ev, ctx) {
761
812
  })));
762
813
  }
763
814
  /**
764
- * `engine_notice` 中性内部通告臂(#310 / #318 件①,契约 = server `ASSISTANT-WIRE-CONTRACT` 附录 D
765
- * + openapi `Event_engine_notice`)。
815
+ * `wiring_manifest.modelGate` 的形校验(core #524 / server S-86,server ≥7.58.0)。
766
816
  *
767
- * ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
768
- * 引擎的**结构化通告**里,被 server 判为面向**本会话终端用户**的那一小撮,按 `sessionId` 路由到
769
- * 这条会话的流上。live 与 durable **两腿都有**(server 三条 run 腿都挂了口)⇒ 断连重连的重放里
770
- * 会**再看到它**,与 `workspace_changed` 同一条**幂等消费**纪律(附录 D.1 逐字)。
817
+ * 语义(core 逐字):**仅本 run 真有门卸时在场**;缺席 = 没卸,**不是**「不知道」也不是
818
+ * 「老引擎」—— 所以消费端拿不到本段时唯一合法的行为是**什么都不渲**,绝不许渲一句
819
+ * 「没有工具被卸掉」(那是把「没报」说成「报了个零」)
820
+ *
821
+ * 三键**全有才收**(与 server 侧同判据)。理由是这一段的用户价值是**一句完整的话**:
822
+ * 「(class)门卸掉了(removed…),恢复用(restore)」—— 少任何一件都只能渲出半句没有出口的话。
823
+ * 缺件 ⇒ 整段不收:不折 `''`、不折空数组、不编默认恢复办法。
824
+ */
825
+ function projectModelGateSection(raw) {
826
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
827
+ return undefined;
828
+ const g = raw;
829
+ const cls = typeof g.class === 'string' && g.class.length > 0 ? g.class : undefined;
830
+ const restore = typeof g.restore === 'string' && g.restore.length > 0 ? g.restore : undefined;
831
+ const removed = Array.isArray(g.removed) && g.removed.every((r) => typeof r === 'string')
832
+ ? g.removed.filter((r) => r.length > 0)
833
+ : undefined;
834
+ if (cls === undefined || restore === undefined || removed === undefined)
835
+ return undefined;
836
+ // 🔴 空数组**不收**:`removed: []` 说的是「卸了零件工具」,与「没卸」在屏面上不可区分,
837
+ // 而 core 的契约是「有门卸才铸段」⇒ 空数组是坏值,不是一个真读数。
838
+ if (removed.length === 0)
839
+ return undefined;
840
+ return { class: cls, removed, restore };
841
+ }
842
+ /**
843
+ * `wiring_manifest.autoMode` 的形校验(core 147③,server ≥7.58.0)。
771
844
  *
772
- * ── 🔴 开集三条(本函数的全部判据,逐条都是「不许做什么」)────────────────────────────────
773
- * **按 `code` + `detail` 消费,`message` 只作 fallback 展示**。core 明写
774
- * `memory.session_polluted` message `memoryProvenance` 模式变文 按 message 文本匹配
775
- * **必碎**(5.41 合流码形退役同教训)。所以本层把三者**分别**上臂,绝不把 detail 折进文案。
776
- * ② **`code` 是开集,认不得也绝不丢帧**。server 的白名单会随 core 码册增长(起步三码 →
777
- * core 5.47/5.48 的 hold 三码入册,附录 D.3 的表尚未跟上)。本层因此**一个码都不硬编**:
778
- * 没有识别表、没有 switch、没有「已知才投」——认不认得是**渲染面**的判断,不是投影面的门。
779
- * ⇒ 本函数对 `code` 唯一的要求是「非空串」(判别键本身缺席才叫畸形)。
780
- * ③ **能力位不当两值门**:本臂不读任何 caps、不问「引擎支不支持 engine_notice」。帧到了就投影
781
- * (A-022「到帧即服务」先例)—— 拿一个探测位去 gate 一条**已经到手的事实**,只会在探测未判/
782
- * 失败时把真事实丢掉。
845
+ * 语义(server 发车帖逐字):**effective 腿恒在**;缺席 = 老 mint / 外部 derive,**不许折成
846
+ * `armed:false`**(那是把「没报」渲成「没武装」)。`reason` **core 六词逐字透传**,
847
+ * 🔴 **不映射** `/v1/capabilities.permissionModeAuto.reason` 的六词 —— 两套词表**同名不同义**
848
+ * (`settings_denied` capabilities 那边折 `no_intent` 不折 `denied`),归一会造出一个假读数。
783
849
  *
784
- * ── 🔴 消费端纪律(写在臂上,因为三端各写一遍必漂)────────────────────────────────────────
785
- * · `detail` 已过 server 的 `redactSecrets` + 尺寸 bound(自由文本 1000 字符;深 4 / 32 /
786
- * 数组 32),**仍按外部串处理**(呈现面字符处理走消费方自己的单源)。
787
- * · `ts` 是 **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。
788
- * · 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
789
- * core 顶注):两个数各自读、并列呈现,任何减法都会得出一个**不存在的量**。本层原样透传 detail
790
- * 正是为了让这条纪律只在渲染面兑现一次,而不是被投影层先算一个差值出来。
791
- * · **本帧的缺席不代表「没发生」**:非白名单码 / 缺 `sessionId` 的通告 server **如实不投**
792
- * (宁缺席不串台),全族那一份始终在 server 的结构化日志里(运维面)。所以消费端**绝不许**
793
- * 从「没收到 engine_notice」反推「记忆姿态正常」。
850
+ * server 侧另有一条**互证**判据:`armed === (reason === 'armed')`,不满足整段不投。本层照抄它,
851
+ * 理由不是「重复上游」而是**非投影口**(宿主自建管线 / 重放存量转录)喂进来的帧不过 server ——
852
+ * 一个 `{armed:true, reason:'settings_denied'}` 会让消费端同时读到「武装了」和「被拒了」。
853
+ */
854
+ function projectAutoModeSection(raw) {
855
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
856
+ return undefined;
857
+ const a = raw;
858
+ const armed = typeof a.armed === 'boolean' ? a.armed : undefined;
859
+ const reason = typeof a.reason === 'string' && a.reason.length > 0 ? a.reason : undefined;
860
+ if (armed === undefined || reason === undefined)
861
+ return undefined;
862
+ if (armed !== (reason === 'armed'))
863
+ return undefined;
864
+ return { armed, reason };
865
+ }
866
+ /**
867
+ * `wiring_manifest` 帧 → 两个超集键的**纯投影**(公面导出;三端共用,壳侧绝不自抄一份形校验)。
794
868
  *
795
- * ── 畸形判据(fail-closed 方向)────────────────────────────────────────────────────────────
796
- * `code` 非串/空串 `malformed`(判别键都没有的通告,渲出去只是一行没有主语的噪声,而 dropped
797
- * 至少会经 `reportDroppedFrame` 留痕)。其余四键**各自**按诚实缺席处理:`message` 非串 ⇒ 空串
798
- * (fallback 位缺席,渲染面据此走纯 code 呈现)、`detail` 非对象 ⇒ 空对象(**不是**丢帧:通告的
799
- * 承重物是 code,detail 坏了不该连带把「这件事发生过」一起吞掉)、`sessionId` / `ts` 非法 键不
800
- * stamp(绝不铸 `0` 这种看起来合法的假读数)。
869
+ * 返回 `undefined` = 这一帧两段都不在场 / 都不成形 ⇒ 调用方**不铸臂**( case 头注)
870
+ * 🔴 帧是 FLAT 的:读 `ev.modelGate` / `ev.autoMode`,**不读** `ev.manifest.*`。
871
+ *
872
+ * 🔴 **刻意不导出**(typeshape 棘轮「unknown 出境只降」逼出来的正解,不是绕过门):本函数的入参是
873
+ * 一个**未校验的 wire 帧**,唯一诚实的声明形就是 `Record<string, unknown>` —— 把它推上公面等于在
874
+ * 包的公面上开一个无类型边界,而那正是那道棘轮存在的理由。三端要消费这两段,走的是**已定型**的
875
+ * chrome 臂 {@link WiringManifestChromeEvent}(判别键 + 两个具名 interface),不是这个原始投影口;
876
+ * 两个具名形({@link WiringManifestModelGate} / {@link WiringManifestAutoMode})照常导出。
877
+ * 若将来真有宿主要自建管线直调本函数,正解是**先给帧定一个具名 interface** 再导出,不是抬棘轮。
801
878
  */
879
+ function wiringManifestSupersetBody(ev) {
880
+ const modelGate = projectModelGateSection(ev.modelGate);
881
+ const autoMode = projectAutoModeSection(ev.autoMode);
882
+ if (modelGate === undefined && autoMode === undefined)
883
+ return undefined;
884
+ return {
885
+ ...(modelGate !== undefined ? { _sema_modelGate: modelGate } : {}),
886
+ ...(autoMode !== undefined ? { _sema_autoMode: autoMode } : {}),
887
+ };
888
+ }
802
889
  function engineNoticeProjection(ev, ctx) {
803
890
  const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
804
891
  if (code === undefined)
@@ -57,10 +57,35 @@
57
57
  * 🔴 `code` 是**开集串**:core 的 `RuleRejectCode` 词表属主是引擎,本包**刻意不镜像那张枚举**
58
58
  * (镜像 = 引擎加员当天把一个合法值判没,#157 词表纪律的反面)。要分支就按串比,未知值原样呈现。
59
59
  */
60
+ /**
61
+ * core 7.3.0(design/382 §3.3-5)—— 关于一条**已被接受**的规则的**宽度提示**。
62
+ * 🔴 **提示不是拒绝**:拒绝集一字未动,这一位说的是「这条规则比这张卡宽,持有人该知道」
63
+ * (`Bash(adb:*)` admits every argument form of `adb`)。渲不渲、怎么渲是端的事;引擎保证的是
64
+ * 另一半 —— 走 edit 臂确认时命中码会写进记录(`edited.warnings`),「宽度在点头之前被说出来过」
65
+ * 因此是可审计的事实。
66
+ * 🔴 `code` 在本包是**开集串**,与本文件 `code?: string` 那一位同一条纪律:core 的
67
+ * `EditedRuleBreadthWarning['code']` 今天是 `'broad_prefix' | 'compound_prefix'` 两员,
68
+ * 镜像那张枚举 = 引擎加员当天把一个合法值判没(#157 词表纪律的反面)。
69
+ */
70
+ export interface EditedRuleBreadthWarning {
71
+ /** 开集串(见类型注)。未知码原样呈现,绝不因为不认得就丢掉这条提示。 */
72
+ readonly code: string;
73
+ /** 基线措辞(端可改写,**绝不可反转**;与 §3.5 的 reason 基线同契约)。 */
74
+ readonly message: string;
75
+ }
60
76
  export type EditedRuleTextPrecheck = {
61
77
  ok: true;
62
78
  /** 真正会落库的那一形(可能与输入字节不同 —— 拼写归一)。内联反馈显示**这一形**。 */
63
79
  canonicalRule: string;
80
+ /**
81
+ * core 7.3.0 跟车一位(B-003 批抬 devDep core ^7.1.0 → ~7.3.0 时由 engine-vocab G4 形对账
82
+ * 抓出:「ok 臂键集恰 {ok, canonicalRule}」当场翻红,core 真返回三键)。**ADDITIVE,缺席 =
83
+ * 没什么可说的**(不是「没有提示 = 规则很窄」的反推位)。
84
+ * 🔴 本包只做**类型面转出**:这条腿是端口注入(值从 core 原样穿过,本层零加工),所以镜像
85
+ * 漏这一位不会剥掉运行期的值,漏掉的是**消费端的类型面可见性** —— 端读不到就不会渲,
86
+ * 「宽度在点头之前被说出来」这件事就只剩引擎那半边。
87
+ */
88
+ warnings?: readonly EditedRuleBreadthWarning[];
64
89
  } | {
65
90
  ok: false;
66
91
  /** 验证器自己拒时在场;coverage 闸拒时缺席(开集串,见类型注)。 */
@@ -9,8 +9,12 @@
9
9
  * recovered → null → 覆盖层摘掉(重试**成功**,不是错误 —— 见下)
10
10
  * gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
11
11
  * 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
12
+ * waiting_first_token → waiting → '✻ Waiting for the model's first token · Ns [/ Ns]'(core 7.3.0 /
13
+ * #530;0.55.0 接。**非错误、非重连**:模型已连上,正在生成前的静默。
14
+ * 携 elapsedMs/timeoutMs;本臂是 error 臂的结构超集 —— 老宿主的
15
+ * else 臂照旧渲 0.54.0 那一行,零行为差异零崩渲,见该臂顶注)
12
16
  * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
13
- * maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**;0.48.0 补齐后两位,
17
+ * maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**、7.3.0 起 +`elapsedMs`/`timeoutMs` 成**十一键**;0.48.0 补齐后两位,
14
18
  * 族扫账见 {@link BRAIN_STATUS_PAYLOAD_KEYS} 末段)。
15
19
  *
16
20
  * 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
@@ -40,6 +44,10 @@ export type RetryStatus =
40
44
  retryAtMs?: number;
41
45
  /** 见 {@link BrainStatusPayload.errorStatus}(引擎给了才在场;缺席禁渲成 0/未知码)。 */
42
46
  errorStatus?: number;
47
+ /** 见 {@link BrainStatusPayload.elapsedMs}(引擎给了才在场;本层零重算)。 */
48
+ elapsedMs?: number;
49
+ /** 见 {@link BrainStatusPayload.timeoutMs}(引擎给了才在场;缺席禁渲成 0)。 */
50
+ timeoutMs?: number;
43
51
  } | {
44
52
  kind: 'error';
45
53
  deadline: number;
@@ -64,24 +72,92 @@ export type RetryStatus =
64
72
  * 渲染措辞是壳半场(本包不渲染),本位只保证那个事实到得了壳。
65
73
  */
66
74
  terminal?: true;
67
- error: {
68
- formatted: string;
69
- isNetworkDown?: boolean;
70
- connection?: {
71
- isSSLError?: boolean;
72
- };
73
- rateLimits?: {
74
- resetsAt?: number;
75
- rateLimitType?: string;
76
- } | null;
77
- };
75
+ error: RetryStatusErrorPayload;
76
+ /** 见 {@link BrainStatusPayload.elapsedMs}(引擎给了才在场;本层零重算)。 */
77
+ elapsedMs?: number;
78
+ /** {@link BrainStatusPayload.timeoutMs}(引擎给了才在场;缺席禁渲成 0)。 */
79
+ timeoutMs?: number;
80
+ }
81
+ /**
82
+ * **等第一个 token**(core 7.3.0 / #530 的 `waiting_first_token` 相,0.55.0 接):请求已经发出、
83
+ * 模型已经连上,只是**还没吐出第一个字**。它既不是错误,也不是重连 —— 没有任何一次尝试失败过,
84
+ * 引擎也不是在**宣告**一次等待,而是在**观测**一次等待(core 顶注逐字:"no wait is being
85
+ * announced, a wait is being OBSERVED")。
86
+ *
87
+ * 🔴 **为什么必须单开一个 kind**(病形:上游闭集加员落 default 臂 ⇒ 新状态被渲成**最像的旧状态**):
88
+ * 0.54.0 及以前本相落 `mapBrainStatusToRetry` 的 `default:` 臂 ⇒ 投成 `{kind:'error'}`,壳把
89
+ * 「在等第一个字」渲成一条**错误色**的 `API error` 行(无 eta、无 attempt)。方向相反的谎报,
90
+ * 与 #3004 修的 `recovered`(重试**成功**被渲成 `API error`)逐字同族。用户报案形正是这一条。
91
+ *
92
+ * 🔴 **本臂刻意是 `error` 臂的结构超集 —— 这是加员的兼容形,不是冗余**:一个还不认得
93
+ * `'waiting'` 的宿主(desktop / web-client 尚未跟车)其 `if (kind==='stalled') … else …` 的
94
+ * else 臂会照旧读到 `error` / `deadline` / `terminal`,渲出与 **0.54.0 逐字节相同**的那一行。
95
+ * 少了 `error` 这一位,老宿主的 `status.error.rateLimits` 就是一次 TypeError —— 渲染路径裸抛
96
+ * = 整屏崩([render-path-must-not-throw]),把「一行渲错」升级成「一屏全没」。
97
+ * 判据写进门:`run-client-core-pure-test.mjs` G6-d「老宿主归约视图对本相的输出 ≡ 0.54.0 对
98
+ * 同一 payload 的输出」。认得本 kind 的宿主拿 {@link elapsedMs} / {@link timeoutMs} 渲真话。
99
+ */
100
+ | {
101
+ kind: 'waiting';
102
+ /**
103
+ * 🔴 **兼容位,不是一个真等待的截止点**:core 明说本相**绝不带** `retryInMs`/`retryAtMs`
104
+ * (它不宣告等待),故本位恒 = 映射时刻的 `nowMs` ⇒ 渲染面按「剩余量 ≤ 0」自然不渲倒计时。
105
+ * 它在场只为让**老宿主**的 else 臂算得出数而不是 NaN;**认得本 kind 的宿主不得拿它渲倒计时**
106
+ * ——「距下次重试」在本相根本没有所指。要渲进度,读 {@link elapsedMs} / {@link timeoutMs}。
107
+ */
108
+ deadline: number;
109
+ attempt?: number;
110
+ maxRetries?: number;
111
+ /** 见 {@link BrainStatusPayload.errClass}(core 不变式:本相恒缺席;本层不写特判,给了就透)。 */
112
+ errClass?: BrainRetryErrClass | (string & {});
113
+ /** 见 {@link BrainStatusPayload.retryAtMs}(core 不变式:本相恒缺席;本层不写特判,给了就透)。 */
114
+ retryAtMs?: number;
115
+ /** 见 {@link BrainStatusPayload.errorStatus}(core 不变式:本相恒缺席;本层不写特判,给了就透)。 */
116
+ errorStatus?: number;
117
+ /** 已经等了多久(ms)。见 {@link BrainStatusPayload.elapsedMs};缺席 = 引擎没给,禁渲 0。 */
118
+ elapsedMs?: number;
119
+ /** 这次等待的天花板(ms)。见 {@link BrainStatusPayload.timeoutMs};缺席 = 不渲「/ Ns」。 */
120
+ timeoutMs?: number;
121
+ /**
122
+ * 🔴 **恒缺席**(`?: never`):等第一个 token **不是**终态 —— 引擎还在等,首 token 到了会补
123
+ * `recovered`、彻底失败会补 `gave_up`。这一位写在型上只为让**老宿主**那句 `status.terminal`
124
+ * 在加员后仍然编译得过(union 各员都得有这个键才允许直接读),值上永不 stamp。
125
+ */
126
+ terminal?: never;
127
+ /**
128
+ * 🔴 **兼容位**:`formatted` 恒为空串 —— 与 0.54.0 的 `default:` 臂逐字节相同(那一臂也是
129
+ * `{ formatted: '' }`)。本相**没有错误**,所以这里也不许编一句错误文案出来;它存在的唯一
130
+ * 理由是老宿主的 else 臂要读得到这个对象(见上文 G6-d)。新宿主**不得**读本位当错误证据 ——
131
+ * 判「有没有错」的量是 `kind`,不是这个恒空的壳([anchor-on-the-deciding-quantity])。
132
+ */
133
+ error: RetryStatusErrorPayload;
78
134
  };
135
+ /**
136
+ * `error` 载荷的**具名形**。
137
+ * 🔴 具名不是风格偏好,是 typeshape 门 B4 的硬要求:导出签名里的**内联匿名形(≥3 成员)**受棘轮
138
+ * 管(只降不升)—— 0.55.0 的 `waiting` 臂复用同一个形,再内联抄一份就是把同一个结构写第三遍、
139
+ * 并把棘轮顶高一格。具名后结构与此前**逐字节相同**(TS 结构化类型 ⇒ 对消费端零差异,不是收窄),
140
+ * 而且这一形从此只有**一处**定义 —— 三个臂再也不可能各自漂。
141
+ * 🔴 **刻意不导出**:它是 `RetryStatus` 的内部构件,公面契约仍是 `RetryStatus` 本身
142
+ * (公面导出表是登记制,加一个名字要走那张表;这一位没有独立的消费理由)。
143
+ */
144
+ interface RetryStatusErrorPayload {
145
+ formatted: string;
146
+ isNetworkDown?: boolean;
147
+ connection?: {
148
+ isSSLError?: boolean;
149
+ };
150
+ rateLimits?: {
151
+ resetsAt?: number;
152
+ rateLimitType?: string;
153
+ } | null;
154
+ }
79
155
  /**
80
156
  * core `BrainStatusPhase` 的**运行期值拷贝**(engine-vocab 门 G2-b 拿它对实装 core 对账)。
81
157
  * 引擎侧是闭集,**读时按开集处理** —— 一个部署可以在自己的通道上冒出别的相,认得的分支照走,
82
158
  * 认不得的落 `default:` 兜底(见 {@link mapBrainStatusToRetry} 末臂)。
83
159
  */
84
- export declare const BRAIN_STATUS_PHASES: readonly ["rate_limited", "retrying", "reconnecting", "circuit_open", "recovered", "gave_up"];
160
+ export declare const BRAIN_STATUS_PHASES: readonly ["rate_limited", "retrying", "reconnecting", "circuit_open", "recovered", "gave_up", "waiting_first_token"];
85
161
  export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
86
162
  /**
87
163
  * core `BrainRetryErrClass`(5.43.0)的**类型面镜像** —— 一次重试等待的**原因分桶**,
@@ -99,8 +175,8 @@ export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
99
175
  * 判据锚在「真正决定结果的量」上:决定结果的是有没有分支,不是有没有一张表。
100
176
  */
101
177
  export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'rate_limit' | 'server' | 'http' | 'output_cap';
102
- /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 **9** 键 ——
103
- * 真源 = server 7.54.0 `dist/trace/project.js` 的 `brainStatusEventData`,逐条条件拷贝)。 */
178
+ /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 **11** 键(7.3.0 起含 `elapsedMs`/`timeoutMs`)——
179
+ * 真源 = server 7.58.0 `dist/trace/project.js` 的 `brainStatusEventData`,逐条条件拷贝)。 */
104
180
  export interface BrainStatusPayload {
105
181
  /** 闭集 + `(string & {})`:未知相仍可携带(开集读),不必先改类型再解析。 */
106
182
  phase: BrainStatusPhase | (string & {});
@@ -162,13 +238,32 @@ export interface BrainStatusPayload {
162
238
  * 分工:本包只负责让这个事实到得了壳(投影/透传保真),措辞与是否上屏是壳半场。
163
239
  */
164
240
  errClass?: BrainRetryErrClass | (string & {});
241
+ /**
242
+ * core 7.3.0(#530;server ≥7.58 `brainStatusEventData` 真发;ADDITIVE,0.55.0 补)——
243
+ * 在一帧 `waiting_first_token` 上:**第一个 token 已经被等了多久**(ms,发帧那一刻,发帧进程
244
+ * 自己的钟;跨进程跳是近似值)。core 顶注:`Absent on every other phase`。
245
+ *
246
+ * 🔴 缺席禁折 0:「不知道等了多久」与「等了 0 毫秒」是两件事,后者会让屏上那行秒数恒 0
247
+ * ([honest-absence-not-fabricated-zero])。
248
+ */
249
+ elapsedMs?: number;
250
+ /**
251
+ * core 7.3.0(#530;server ≥7.58 `brainStatusEventData` 真发;ADDITIVE,0.55.0 补)——
252
+ * 在一帧 `waiting_first_token` 上:**已武装的首 token 看门狗**(ms)= 这次等待的天花板,
253
+ * 消费方据它渲「45s of 120s」。core 顶注:`Absent on every other phase`。
254
+ *
255
+ * 🔴 与 `retryInMs` 不是同一个量,**绝不可互换**:那一位是「还要等多久才重试」(引擎在宣告一次
256
+ * 等待),本位是「这次等待最多容忍多久」(引擎在观测一次等待)。把本位当倒计时渲,渲出来的是
257
+ * 一个从不递减的数。
258
+ */
259
+ timeoutMs?: number;
165
260
  }
166
261
  /**
167
262
  * 上面那个 interface 的**运行期键镜像**(照 `TOOL_APPROVAL_FRAME_KEYS_MIRROR` 先例):
168
263
  * engine-vocab 门 G2-c 拿它与 core `BrainStatus` 的键集逐元素比 ⇒ 引擎 additive 增键当天红。
169
264
  * 下面两个类型钉保证镜像与 interface 之间不可能漂移(少键/多键都是编译错)。
170
265
  */
171
- export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass", "retryAtMs", "errorStatus"];
266
+ export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass", "retryAtMs", "errorStatus", "elapsedMs", "timeoutMs"];
172
267
  /**
173
268
  * BrainStatus 载荷 → spinner 行状态。
174
269
  *
@@ -176,3 +271,4 @@ export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "re
176
271
  * 已经成功,行就该摘掉。此前本函数返回型钉死非空,于是 `recovered` 只能被塞进 error 臂。
177
272
  */
178
273
  export declare function mapBrainStatusToRetry(p: BrainStatusPayload, nowMs: number): RetryStatus | null;
274
+ export {};
@@ -9,8 +9,12 @@
9
9
  * recovered → null → 覆盖层摘掉(重试**成功**,不是错误 —— 见下)
10
10
  * gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
11
11
  * 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
12
+ * waiting_first_token → waiting → '✻ Waiting for the model's first token · Ns [/ Ns]'(core 7.3.0 /
13
+ * #530;0.55.0 接。**非错误、非重连**:模型已连上,正在生成前的静默。
14
+ * 携 elapsedMs/timeoutMs;本臂是 error 臂的结构超集 —— 老宿主的
15
+ * else 臂照旧渲 0.54.0 那一行,零行为差异零崩渲,见该臂顶注)
12
16
  * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
13
- * maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**;0.48.0 补齐后两位,
17
+ * maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**、7.3.0 起 +`elapsedMs`/`timeoutMs` 成**十一键**;0.48.0 补齐后两位,
14
18
  * 族扫账见 {@link BRAIN_STATUS_PAYLOAD_KEYS} 末段)。
15
19
  *
16
20
  * 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
@@ -35,6 +39,11 @@ export const BRAIN_STATUS_PHASES = [
35
39
  'circuit_open',
36
40
  'recovered',
37
41
  'gave_up',
42
+ // core 7.3.0 跟车一相(#530;0.55.0)——「等第一个 token」。加员的**理由**与 recovered 那一相
43
+ // 逐字同族:少一相 ⇒ 它落 `mapBrainStatusToRetry` 的 default 臂被渲成错误行。engine-vocab G2-b
44
+ // 对**实装 core** 逐词对账(本批 devDep ^7.1.0 → ^7.4.0 后那道门先红后绿,红文逐字:
45
+ // 「漏:waiting_first_token」)。
46
+ 'waiting_first_token',
38
47
  ];
39
48
  /**
40
49
  * 上面那个 interface 的**运行期键镜像**(照 `TOOL_APPROVAL_FRAME_KEYS_MIRROR` 先例):
@@ -59,6 +68,11 @@ export const BRAIN_STATUS_PAYLOAD_KEYS = [
59
68
  // engine-vocab G2-c 的等值门本批**先红后绿**,红文逐字:「漏:retryAtMs,errorStatus」。
60
69
  'retryAtMs',
61
70
  'errorStatus',
71
+ // ── core 7.3.0 跟车**两键**(0.55.0;#530)────────────────────────────────────────────────
72
+ // 与 4) 的相位加员同一批上游变更:core 在 `BrainStatus` 上加了 elapsedMs/timeoutMs 两位,
73
+ // server 7.58.0 的 `brainStatusEventData` 白名单同批放行。镜像不跟 ⇒ G2-c 等值门红。
74
+ 'elapsedMs',
75
+ 'timeoutMs',
62
76
  ];
63
77
  const _brainStatusKeyPin = [true, true];
64
78
  void _brainStatusKeyPin;
@@ -97,7 +111,24 @@ export function mapBrainStatusToRetry(p, nowMs) {
97
111
  */
98
112
  const producerTiming = typeof p.retryAtMs === 'number' ? { retryAtMs: p.retryAtMs } : {};
99
113
  const failureStatus = typeof p.errorStatus === 'number' ? { errorStatus: p.errorStatus } : {};
100
- const extra = { ...counts, ...cause, ...producerTiming, ...failureStatus };
114
+ /**
115
+ * core 7.3.0 跟车两位(0.55.0),**原样透传、零重算**,并且**与其它键一样进 `extra`**
116
+ * (= 每个臂都透,不按相位写特判)。理由与上面 `producerTiming`/`failureStatus` 那段逐字同源:
117
+ * core 的不变式说这两位只在 `waiting_first_token` 上在场 —— 但**由 core 说,不由本层复述**。
118
+ * 本层若写「只有 waiting 臂才透 elapsedMs」,上游哪天把它铺到别的相,本层就成了单方面剥键
119
+ * (`task_progress.model` / `retryAtMs` 两次同形存量的病根)。判据锚在真正决定渲染的量
120
+ * (`kind`),不锚一个恰好同时成立的第二事实([anchor-on-the-deciding-quantity])。
121
+ */
122
+ // 两个进度位是引擎快照(ms):负数 / NaN / Infinity 不是「等了负久」,一律当缺席(0.55.0 发包扫描 [low] 采;
123
+ // 同一包的 eventToSdkMessage 腿对数值位同样只认有限非负数,两条腿同律)。
124
+ const saneMs = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
125
+ const elapsedMs = saneMs(p.elapsedMs);
126
+ const timeoutMs = saneMs(p.timeoutMs);
127
+ const waitProgress = {
128
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
129
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
130
+ };
131
+ const extra = { ...counts, ...cause, ...producerTiming, ...failureStatus, ...waitProgress };
101
132
  switch (p.phase) {
102
133
  // 🔴 引擎直报「恢复」:摘行。绝不落 error 臂 —— 那是把成功渲成失败。
103
134
  case 'recovered':
@@ -119,6 +150,19 @@ export function mapBrainStatusToRetry(p, nowMs) {
119
150
  // 靠 attempt===maxRetries 去推是错的(供给方根本不发那两位)。
120
151
  case 'gave_up':
121
152
  return { kind: 'error', deadline, terminal: true, ...extra, error: { formatted: p.detail ?? '' } };
153
+ /**
154
+ * 🔴 core 7.3.0 / #530 加的第七相 —— 等第一个 token。**必须在 default 臂之前落自己的 kind**:
155
+ * 这一相走到 `default:` 就是本 bug 的根因(B-003 / L-108①)——「模型正在生成前的静默」被投成
156
+ * `{kind:'error'}`,壳照 `kind` 渲成错误色的 `API error` 行,用户看到的是「在重试」,而事实是
157
+ * 「在等第一个字」。这不是措辞问题:方向是反的(没有任何一次尝试失败过)。
158
+ *
159
+ * `error: { formatted: '' }` 与 `deadline` 是**兼容位**(见 `RetryStatus` 的 `waiting` 臂顶注):
160
+ * 老宿主的 else 臂据它们渲出与 0.54.0 **逐字节相同**的那一行 ⇒ 加员对未跟车的端零行为差异、
161
+ * 零崩渲。`terminal` 永不 stamp —— 首 token 到了 core 补 `recovered`(壳摘行),彻底失败补
162
+ * `gave_up`(壳翻终态错误行),等待本身从来不是终态。
163
+ */
164
+ case 'waiting_first_token':
165
+ return { kind: 'waiting', deadline, ...extra, error: { formatted: '' } };
122
166
  case 'retrying':
123
167
  default:
124
168
  return { kind: 'error', deadline, ...extra, error: { formatted: '' } };
package/dist/seam.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
10
10
  import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
11
+ import type { WiringManifestAutoMode, WiringManifestModelGate } from './adapter/downstream/eventToSdkMessage.js';
11
12
  /**
12
13
  * `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
13
14
  *
@@ -389,7 +390,36 @@ export type ChromeEvent = {
389
390
  * `actor.hostAsserted` 是消费端唯一能判「这个署名可信吗」的位:渲署名而不渲这个位 = 把一个
390
391
  * 未经验证的名字渲成可信的(core 自己的 `[from …]` 渲染就是靠它决定加不加 `(unverified)`)。
391
392
  */
392
- | HumanInputChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent;
393
+ | HumanInputChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | WiringManifestChromeEvent;
394
+ /**
395
+ * {@link ChromeEvent} 的 `wiring_manifest` 臂(core #524 + core 147③,server ≥7.58.0)——
396
+ * 引擎接线自述里**两段面向终端用户的事实**,其余每一段仍不投影(射程见 eventToSdkMessage 的
397
+ * `case 'wiring_manifest'` 头注)。
398
+ *
399
+ * ── 🔴 宿主消费义务(三条,全部是「不许做什么」)────────────────────────────────────────────
400
+ * ① **缺席不可反推**。`modelGate` 缺席 = 本 run 没有门卸(core 只在真卸时铸段),
401
+ * `autoMode` 缺席 = 老 mint / 外部 derive **没报**。两者都**不许**被渲成一句肯定句
402
+ * (「没有工具被卸掉」/「auto 未武装」)—— 那是把「没报」说成「报了个否」。
403
+ * 本臂在两段都不成形时**根本不会到达**,所以宿主见到本臂就至少有一段是真读数。
404
+ * ② **`autoMode.reason` 六词逐字呈现,不许映射**到 `/v1/capabilities.permissionModeAuto.reason`
405
+ * 的六词:两套词表**同名不同义**(`settings_denied` 在 capabilities 那边折 `no_intent`
406
+ * 不折 `denied`)。要两面都说,就两面各自读、各自渲,绝不归一。
407
+ * ③ **`modelGate.restore` 原样呈现**:它是 core 铸的**逐字**恢复办法,宿主自己拼一句
408
+ * 「试试把某某开关关掉」等于替引擎编了一条它没说过的出口。
409
+ * 🔴 **幂等**:durable 腿重放会再送同一帧(与 `workspace_changed`/`engine_notice` 同纪律),
410
+ * 宿主按 run/leg 去重,别按到达次数计数。
411
+ * 缺席(宿主不接本臂)= 这两条披露在该宿主上看不见,**不是**报错。
412
+ */
413
+ export interface WiringManifestChromeEvent {
414
+ kind: 'wiring_manifest';
415
+ laneProof: LaneProof;
416
+ /** 仅本 run 真有门卸时在场(见义务①)。 */
417
+ modelGate?: WiringManifestModelGate;
418
+ /** effective 腿恒在;缺席只表示「没报」(见义务①)。 */
419
+ autoMode?: WiringManifestAutoMode;
420
+ /** core 铸的事件身份(uuidv7 形);wire 未必带 ⇒ 缺席时本键不在场。 */
421
+ eventId?: string;
422
+ }
393
423
  /**
394
424
  * {@link ChromeEvent} 的 `text_segment_end` 臂(#323 / core #447,core ≥5.63 / server ≥7.50)——
395
425
  * 「**assistant 的这一段散文写完了**」,由引擎明说,不是由本包猜。
package/dist/seam.js CHANGED
@@ -48,6 +48,12 @@ const CHROME_ARM_TABLE = {
48
48
  required: false,
49
49
  duty: '可选:渲引擎通告(按 code+detail,message 仅 fallback;未知 code 也必须渲不许丢;durable 重放按 eventId 幂等;harvest 的 moved/escalated 并列呈现绝不相减)',
50
50
  },
51
+ wiring_manifest: {
52
+ required: false,
53
+ duty: '可选:渲引擎接线自述里的两段用户面事实(modelGate = 本 run 被模型门卸掉的工具 + 逐字恢复办法;' +
54
+ 'autoMode = 武装位 + core 六词原因)。🔴 两段缺席一律不渲肯定句;reason 绝不映射 capabilities 六词;' +
55
+ 'restore 原样呈现;durable 重放按 run/leg 幂等',
56
+ },
51
57
  text_segment_end: {
52
58
  required: false,
53
59
  duty: '可选:引擎明报的 assistant 散文段边界(#323/core #447)。🔴 content 是对账/定界用的权威全文,拿它再渲一行 = 同一段上屏两遍;缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判',
@@ -117,6 +117,14 @@ export const STRUCTURED_DETAIL_TYPES = new Set([
117
117
  // 病根仍是对账物代际差:门锚本仓 devDep core,devDep 停 5.20.0 时该词还没铸 ⇒ 门恒绿。
118
118
  // 修 = devDep 升 ^5.43.0(门当场翻红显出漂移)+ 补词,与 A-004.5 那批同一条路。
119
119
  'read_path_denied',
120
+ // ── core 7.3.0 跟车一词(B-003 批,2026-09-05;engine-vocab ⑤ 段对 7.3.1 实装物直证)──────────
121
+ // 🔴 **不是本批点名的件,是抬对账物时族扫捞出来的存量漏词**([same-shape-residue-constitution]):
122
+ // 本批为了让 G2-b 对 `waiting_first_token` 有判别力,把 devDep core 从 ^7.1.0 抬到 ~7.3.0;
123
+ // 门当场翻红,红文逐字:「core 7.3.1:43 项 / 本包 42 项;漏:list-agents」。
124
+ // `list-agents` = ListAgents 工具的结构化卡(core `dist/agents/list-agents-tool.js` 铸点,
125
+ // 并已登记在 `dist/core/runner/tool-output-projection.js:67` 的 `CC_DETAIL_TYPES` 里)。
126
+ // 病根与 5.20 / 5.43 两次逐字同族:词漏了不会响,只会让那一类卡**永远退回正则解模型面文本**。
127
+ 'list-agents',
120
128
  ]);
121
129
  // 🔴 同批**删三词**(core 5.10.0 BREAKING「幽灵卡」清仓):`multiedit` / `memory-saved` /
122
130
  // `memory-recall` —— 全树零铸点(MultiEdit/批量重放铸的是 `type:"edit"` 带 `edits[]`;core
@@ -19,11 +19,11 @@
19
19
 
20
20
  | 项 | 值 | 真源 |
21
21
  |---|---|---|
22
- | 本包 | `@sema-agent/client-core` **0.54.0**(工作树**未发**;npm 最新 = **0.53.0**。design/385 那一批进 `CHANGELOG.md` 的 `## 0.54.0(2026-09-05)` 段,冻结账已按两阶段协议插 `pending` 行) | `package.json` `version` |
22
+ | 本包 | `@sema-agent/client-core` **0.55.0**(工作树**未发**;npm 最新 = **0.54.0**。B-003 `waiting_first_token` 那一批进 `CHANGELOG.md` 的 `## 0.55.0(2026-09-05)` 段,冻结账已按两阶段协议插 `pending` 行) | `package.json` `version` |
23
23
  | peer:wire 契约 | `@sema-agent/sdk` **>=7.4.0**(value-level,非 type-only;0.48.0 抬版,四条硬理由见 `CHANGELOG.md` 0.48.0 段末的地板影响面账) | `package.json` `peerDependencies` |
24
24
  | peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
25
25
  | runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
26
- | 公开导出面 | **815** 个运行期符号(+ 41 个测试钩;= 工作树当下的值 —— 再加 design/385 十件未发 additive 导出(0.54.0 段,含发包扫描补的 `AUTHORITY_ENVELOPE_TAGS`);已发的 `0.51.0` 是 **803**,再加 L-69⑨ 两件未发 additive 导出;`0.49.0` 是 **795**,再加 S-81 五件未发 additive 导出;`0.48.0` 是 **794**,npm `0.47.0` 是 **790**,`0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **815** 个运行期符号(+ 41 个测试钩;= 工作树当下的值 = 已发 `0.54.0`(design/385 十件 additive `AUTHORITY_ENVELOPE_TAGS`);`0.52.0` 是 **805**(L-69⑨ 两件);`0.51.0` **803**;`0.50.0` 是 **800**(S-81 五件);`0.49.0` **795**;`0.48.0` 是 **794**;npm `0.47.0` 是 **790**,`0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
27
27
  | 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
28
28
  | 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
29
29
 
@@ -31,13 +31,11 @@
31
31
  (`readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` / `readLastCapture`)
32
32
  在旧版上按名 import 会**在 ESM 实例化当场炸**(具名导出不存在)—— 提货前先抬依赖。
33
33
  装 ≤`0.48.0` 的端同理:`0.49.0` 的 `projectCrashConverged`(L-38,§12)在旧版上不存在。
34
- ⚠️ **工作树里还有一批比 `0.49.0` 更晚的未发 additive 导出**(S-81,见 §13):
35
- `classifySelfOrchestrationRefusal` / `stripSelfOrchestrationIntent` / `projectWorkflowsGate` /
36
- `SELF_ORCHESTRATION_RETRY_WITHOUT` / `CAPABILITY_SELF_ORCHESTRATION_REQUIRED`
37
- (+ **四个** type-only `SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` /
38
- `WorkflowsGateProjection` / `WorkflowsGateUnknownDenial`)。它们随下一个版本段发出;
39
- 在此之前按名 import 会在 ESM 实例化当场炸 —— 提货前先抬依赖。
40
- (L-38 的 `projectCrashConverged` 已随 `0.49.0` 发出,不再是未发件。)
34
+ ≤`0.49.0` 的端同理:`0.50.0` S-81 五件(`classifySelfOrchestrationRefusal` / `stripSelfOrchestrationIntent` /
35
+ `projectWorkflowsGate` / `SELF_ORCHESTRATION_RETRY_WITHOUT` / `CAPABILITY_SELF_ORCHESTRATION_REQUIRED`
36
+ + **四个** type-only 形 `SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` / `WorkflowsGateProjection` / `WorkflowsGateUnknownDenial`,§13)、
37
+ ≤`0.53.0` 的端同理:`0.54.0` design/385 十件(§17)—— 在旧版上按名 import 会在 ESM 实例化当场炸,提货前先抬依赖。
38
+ (「未发」告示按「发出即退场」约定退场:本档只列**已发版本**的下限,不再预告工作树里的未发件。)
41
39
  🔴 **0.48.0 还抬了 peer 地板**(`@sema-agent/sdk >=7.4.0`),这是本版**唯一**的非 additive 面:
42
40
  端装 <7.4.0 的 SDK 会看到 peer 警告(运行期不因此变化)。同一条对 0.47.0 那 **3 个 additive 导出**
43
41
  成立(`planInteractiveHalt` / `RUN_LEVEL_STOP_ERROR_CODES` / `readDecideCurrentPending`)。同一条对 0.38.0 那 11 个
@@ -237,12 +235,18 @@ type EventProjection =
237
235
  `context_usage` / `config_assembled` / `message_committed`(引擎可观测/审计面,CC transcript 无对位物)·
238
236
  `needs_review`(壳消费的是 `done{status:'needs_review'}` 终帧,事件形重复且更早)·
239
237
  `compaction_outcome`(压缩**非 compacted 结局**报告;CC transcript 无对位物,见 §7 缺口 **P-6**)·
240
- `wiring_manifest`(引擎接线自述)· `tool_approval` / `tool_approval_complete`(**不是丢帧**:它们走
238
+ `tool_approval` / `tool_approval_complete`(**不是丢帧**:它们走
241
239
  `hitl/toolApprovalWire.isToolApprovalFrame` + `hitl/frameRouter` 那条审批卡链;在这里投一条 transcript 行
242
240
  只会让同一只 ask 出现两次)· **`error`**(见下,单列)。
243
241
 
244
242
  <!-- /ARM-SET:not_in_slice -->
245
243
 
244
+ 📌 **`wiring_manifest` 自 0.56.0 起离开本表,但它只被接了两段**(L-70 / L-108②③)——
245
+ `modelGate` 与 `autoMode` 投成内部臂 + chrome 臂(§19),整份 manifest 的**其余十一段**
246
+ (leg / ask / question / interaction / elicit / parkLane / session / fleet / permissionRules /
247
+ governance / configFingerprint)仍然一件没接。两段都不成形的那些帧照旧回 `none/not_in_slice`,
248
+ **不铸空臂**。⇒ 别把「wiring_manifest 不在这张表里了」读成「manifest 接上了」。
249
+
246
250
  🔴 **`error` 单列点名**(端最容易据 §3a 的规则推错的一条):它的名字骗人 —— 那是流的 **15 分钟帽帧**
247
251
  (server `sse-log.ts`,已知 `errorCode` = `STREAM_MAX_DURATION`),帧自己就说 **run 仍然活着**;
248
252
  终态臂只有 `done` / `failed`。本切片对它无对位渲染物,所以走 `nothing('not_in_slice')` ——
@@ -2694,3 +2698,172 @@ module 台账一个都没动(它们判的是「哪个 task 的哪个状态」,
2694
2698
  **实现锚**:`src/peerFrames.ts`(`classifyPeerNotification` / `renderPeerFrameTranscriptText` /
2695
2699
  `parsePeerFrameText` / `peerFrameDisplayName` / 三张闭集表)、`src/adapt/arms.ts` 的通知臂。
2696
2700
  **常驻门**:`scripts/run-peer-frame-projection-test.mjs`(71 checks,含适配器级的两条台账回归)。
2701
+
2702
+ ## §18 🆕 `waiting_first_token` —— 等第一个 token 的相(0.55.0;core 7.3.0 #530 / server ≥7.58;cli B-003 / L-108①)
2703
+
2704
+ ### 18a. 修的是哪一形
2705
+
2706
+ `BrainStatusPhase` 是**闭集**,而消费端对它有 `switch`。core 7.3.0 加了第七相
2707
+ `waiting_first_token`(首 token 看门狗已武装、第一个字还没到;等满 `WAITING_FIRST_TOKEN_AFTER_MS`
2708
+ = 30s 起每 30s 一帧,携 `elapsedMs` / `timeoutMs`;铸点 `core/dist/brain/stream-engine.js`
2709
+ 的 `emitWaitFrame`)。0.54.0 及以前本包不认得它 ⇒ 落 `mapBrainStatusToRetry` 的 `default:` 臂
2710
+ 被投成 `{kind:'error'}`。用户端两个面:
2711
+
2712
+ | 引擎 | 用户看到 | 事实 |
2713
+ |---|---|---|
2714
+ | 7.57.0(core 7.2.0) | 什么都没有 | 相不存在 |
2715
+ | 7.58.0(core 7.3.1) | 一条**错误色**的 `API error` 行 | 模型已连上,正在生成前的静默;**一次尝试都没失败过** |
2716
+
2717
+ 病形 = 「上游闭集加员落 default 臂 ⇒ 新状态被渲成**最像的旧状态**」,与 #3004 修的 `recovered`
2718
+ (重试**成功**被渲成 `API error`)逐字同族。
2719
+
2720
+ ### 18b. 端该怎么用
2721
+
2722
+ ```ts
2723
+ import { mapBrainStatusToRetry, type RetryStatus } from '@sema-agent/client-core'
2724
+
2725
+ const st = mapBrainStatusToRetry(brainStatusPayload, Date.now())
2726
+ if (st === null) return null // recovered:摘掉覆盖层
2727
+ if (st.kind === 'waiting') { // 🆕 0.55.0
2728
+ // 非错误色。elapsedMs 缺席 ⇒ 不渲秒数(绝不折 0);timeoutMs 缺席 ⇒ 不渲「/ Ns」。
2729
+ return renderWaitingRow({ elapsedMs: st.elapsedMs, timeoutMs: st.timeoutMs })
2730
+ }
2731
+ if (st.kind === 'stalled') return renderStalledRow(st) // reconnecting:网络面
2732
+ return renderErrorRow(st) // 其余:重试/限流/熔断/放弃
2733
+ ```
2734
+
2735
+ 三条**必须**照做的纪律:
2736
+
2737
+ 1. **不得拿 `deadline` 渲倒计时**。本相不带 `retryIn*`(core:它不宣告等待,它**观测**等待),
2738
+ `deadline` 恒 = 映射时刻的 `nowMs`,是**兼容位**(见 18c),不是一个真的截止点。
2739
+ 2. **不得拿 `timeoutMs` 当倒计时源**。它是这次等待的**天花板**(渲「45s of 120s」的分母),
2740
+ 拿它当「还要等多久」会渲出一个从不递减的数。
2741
+ 3. **缺席禁折 0**。两位都可能缺席(旧 server 不发 / 未来相位铺开),缺席 = 不知道,不渲。
2742
+
2743
+ ### 18c. 🔴 老宿主:**结构形免动,穷尽形必须加一条 case**
2744
+
2745
+ `waiting` 臂刻意是 `error` 臂的**结构超集**:它带 `deadline` / `attempt` / `maxRetries` /
2746
+ `errClass` / `retryAtMs` / `errorStatus` / `error{formatted:''}`,`terminal` 恒缺席。于是一个还没加
2747
+ `kind === 'waiting'` 分支的端,其 `if (kind==='stalled') … else …` 的 else 臂读到的一切与 0.54.0
2748
+ **逐位相同**,渲出的行也逐字节相同。
2749
+
2750
+ **为什么不用「更干净」的瘦臂**:少了 `error` 这一位,结构形老宿主的 `status.error.rateLimits` 就是
2751
+ 一次 TypeError —— 渲染路径裸抛 = 整屏崩,把「一行渲错」升级成「一屏全没」。加员制造崩渲是不可接受
2752
+ 的代价;让未跟车的端**保持原样**才是加员的诚实形。
2753
+
2754
+ 🔴 **但结构超集救不了穷尽形消费者,这一条必须说清楚,不许含混成「无需动作」**(异源对抗复审
2755
+ [medium] 采纳):`switch (status.kind) { case 'stalled': …; case 'error': …; default: assertNever(status) }`
2756
+ 这一形破的是**判别值**本身,不是形状 —— 补多少结构位都没用。后果两面:重新编译时报
2757
+ 「`waiting` 不可赋给 `never`」(编译错,响亮,好);**已经编译好**的老产物拿到新 kind 会在
2758
+ default 臂**抛**(运行期,安静得多)。⇒ 对这一类消费方,0.55.0 是 **BREAKING**,发布帖必须点名。
2759
+ 两形的事实各自钉在门里:`run-client-core-pure-test.mjs` G6-e ①(结构形零差异)/ ②(穷尽形真抛)
2760
+ / ③(反空转:加员**前**同一个穷尽形消费者**不抛** ⇒ ② 的抛精确归因于多了一个判别值)。
2761
+
2762
+ 🔴 **第三形:不读 `kind` 的宿主**(0.55.0 发包扫描 [medium] 采,按实际消费者取来的分类):web-client 现状是把
2763
+ `retryStatus` 非空一律渲成既有错误行「Request failed · retrying (n/m)」,根本不看 `kind`。它在 waiting 相上
2764
+ 渲的仍是那一行 —— 与 0.54.0 **逐字节相同**,所以不是回归;但用户面拿不到进度行,要接 §18b(读 `kind==='waiting'`
2765
+ 渲 headline + `elapsedMs`/`timeoutMs`)才有真话。三形对照:结构形零差异 / 穷尽形 BREAKING / 不读 kind 形=旧行不变。
2766
+
2767
+ 判据不是一句承诺,是两道门(拿**本函数自己的 `default:` 臂**当活对照 —— 一个未知相走的正是加员前
2768
+ 第七相走的那条臂 —— 两边在老宿主读得到的键上逐位相等才绿):
2769
+ `scripts/run-client-core-pure-test.mjs` G6-d(单元)/ `scripts/run-additive-key-passthrough-test.mjs`
2770
+ G5d(全链两段)。
2771
+
2772
+ ### 18d. `BrainStatus` 九键 → 十一键
2773
+
2774
+ 新增 `elapsedMs`(已经等了多久,ms)与 `timeoutMs`(这次等待的天花板,ms)。两位**进 `extra` 统一
2775
+ 透传**,不按相位写特判 —— core 的不变式(「只在本相在场」)由 core 说,本层复述一遍就等于上游哪天
2776
+ 把它铺到别的相时本层单方面剥键(`task_progress.model` / `retryAtMs` 两次存量的病根)。
2777
+
2778
+ 🔴 **族扫落点两处**:`adapter/downstream/eventToSdkMessage.ts` 的 `case 'status'`(wire → 内部臂)
2779
+ 与 `adapt/arms.ts` 的 `retryStatusArm`(内部臂 → chrome 事件)。漏任一层,新相到端只剩空壳
2780
+ (kind 对了、秒数永远缺席)。
2781
+
2782
+ ### 18e. 端的措辞(sema 超集,非 CC parity)
2783
+
2784
+ CC 2.1.250 语料**无同形提示行** —— `first token` / `firstToken` / `first_token` 的全部命中都是
2785
+ 账号首 token 日期与流式计时**遥测**,无渲染面;同族里唯一的用户可见行是 `Waiting for API response`,
2786
+ 而那是 `reconnecting` 相的既有行(「在等应答」≠「已连上、在等第一个字」)。⇒ 措辞按 [6386] / L-79②
2787
+ 既定形 `Waiting for the model's first token · Ns [/ Ns]`,各端登记 divergence。
2788
+
2789
+ **cli / web / desktop 认领**:cli 侧渲染臂在本批同车。web / desktop 按 18c **分两形**:
2790
+ 消费形是**结构形**(`if (kind==='stalled') … else …`)⇒ 免动,逐字节零差异;
2791
+ 消费形是**穷尽形**(`switch (kind)` + `default: assertNever`)⇒ **必须加一条 `waiting` case**,
2792
+ 否则重新编译报「waiting 不可赋给 never」、已编译的老产物在 default 臂**抛**。
2793
+ 跟车的正形都是加一条 `kind === 'waiting'` 臂,渲成**非错误色**的进度行。
2794
+
2795
+ **实现锚**:`src/retryStatus.ts`(`RetryStatus` 的 `waiting` 臂 / `BRAIN_STATUS_PHASES` 七相 /
2796
+ `BRAIN_STATUS_PAYLOAD_KEYS` 十一键 / `mapBrainStatusToRetry` 的 `case 'waiting_first_token'`)、
2797
+ `src/adapt/arms.ts`、`src/adapter/downstream/eventToSdkMessage.ts`。
2798
+ **常驻门**:`scripts/run-client-core-pure-test.mjs` G6 组(相位→kind 全矩阵 / 缺席位不捏造 /
2799
+ 老宿主零差异)、`scripts/run-additive-key-passthrough-test.mjs` G5 组(全链两段)、
2800
+ `scripts/run-engine-vocab-floor-test.mjs` G2-b/G2-c(对**实装 core** 逐词逐键等值)。
2801
+
2802
+
2803
+ ## §19 🆕 `wiring_manifest` 的两段用户面事实(0.56.0;core #524 + core 147③ / server ≥7.58.0;cli L-70 / L-108②③)
2804
+
2805
+ ### 19a. 修的是哪一形
2806
+
2807
+ `wiring_manifest` 此前整帧走 `nothing('not_in_slice')`。那对整份 manifest 的**其余十一段**今天仍然
2808
+ 成立(它们是 operator / 诊断面读物,CC transcript 无对位物)。问题只在两段:
2809
+
2810
+ | 段 | 它回答用户的哪个问题 | 不接的后果 |
2811
+ |---|---|---|
2812
+ | `modelGate` | 「这一 run 我为什么少了几个工具?」 | 工具**静默消失**,用户以为模型不听话 |
2813
+ | `autoMode` | 「auto 到底武装了没有?为什么?」 | 只能靠 `/v1/capabilities` 猜,而那套词表**同名不同义** |
2814
+
2815
+ 两段都**没有第二条**到达消费端的路,留在 `not_in_slice` 里就是静默蒸发。
2816
+
2817
+ ### 19b. 端该怎么用(三步,端零字符串判定)
2818
+
2819
+ 1. 接 chrome 臂 `kind: 'wiring_manifest'`(`WiringManifestChromeEvent`)——`CHROME_ARM_TABLE` 里
2820
+ `required:false`:不接 = 这两条披露在该宿主上看不见,不是报错。
2821
+ 2. `modelGate` 在场 ⇒ 渲一行:类别 `class`、被卸工具 `removed[]`、**逐字**的 `restore`。
2822
+ 3. `autoMode` 在场 ⇒ 渲武装位与 `reason` 原文。
2823
+
2824
+ 形已经在包里校验过(见 19c),端**不许再自抄一套形判定**。
2825
+
2826
+ ### 19c. 🔴 五条读法(端不许自己重判,也不许自己放宽)
2827
+
2828
+ 1. **帧是 FLAT 的**:`ev.modelGate` / `ev.autoMode`,**不是** `ev.manifest.*`。6.7.0 契约声明过的
2829
+ 那层 `manifest:{...}` wire 上从未有过(SDK `events.d.ts` 该臂头注 2026-08-07 按真 capture 更正);
2830
+ server 把 core `TaskEvent` 的 `ev.manifest` 过白名单 builder 后 **spread 进帧**,三条腿
2831
+ (live SSE / durable 账本行 / resume 重放)同源。
2832
+ 2. **缺席不可反推**。`modelGate` 缺席 = 本 run 没有门卸(core 只在真卸时铸段);`autoMode` 缺席 =
2833
+ 老 mint / 外部 derive **没报**。两者都**不许**被渲成肯定句(「没有工具被卸掉」/「auto 未武装」)。
2834
+ 两段都不成形时本臂**根本不铸** ⇒ 端见到本臂,至少有一段是真读数。
2835
+ 3. **`autoMode.reason` 绝不映射** `/v1/capabilities.permissionModeAuto.reason` 的六词:两套词表
2836
+ **同名不同义**(`settings_denied` 在 capabilities 那边折 `no_intent` 不折 `denied`)。两面都要说
2837
+ 就两面各自读、各自渲。
2838
+ 4. **`modelGate.restore` 原样呈现**:它是 core 铸的**逐字**恢复办法。端自己拼一句「试试关掉某某开关」
2839
+ 等于替引擎编了一条它没说过的出口。
2840
+ 5. **幂等**:durable 腿重放会再送同一帧(与 `workspace_changed` / `engine_notice` 同纪律),按 run/leg
2841
+ 去重,别按到达次数计数。
2842
+
2843
+ ### 19d. 形校验(包侧已做,两段各自独立)
2844
+
2845
+ - `modelGate`:`class` / `restore` 非空串 ∧ `removed` 是全串数组 ∧ 过滤空串后**非空** —— 三件缺一
2846
+ 整段不收。空 `removed` 与「没卸」在屏面上不可区分,而 core 契约是「有门卸才铸段」⇒ 它是坏值。
2847
+ - `autoMode`:`armed` 是 boolean ∧ `reason` 非空串 ∧ **`armed === (reason === 'armed')`**(server 侧
2848
+ 同一条互证式)。照抄它是因为**非投影口**(宿主自建管线 / 重放存量转录)喂进来的帧不过 server。
2849
+ - 子流断闸按「**键在不在**」判、不按「是不是串」判:本臂决定的是「这份自述算谁的」,一个坏
2850
+ `parentToolCallId` 会把子代的门卸清单擦成 leader 的,用户会以为**自己这一 turn**少了工具。
2851
+
2852
+ ### 19e. 上游供给的诚实边界(本版新增的已知局限)
2853
+
2854
+ - **超集键名是临时形**:`modelGate` / `autoMode` 今天**不在**已发布 SDK(8.1.0)的 `AgentEvent`
2855
+ wiring_manifest 臂里(全树零命中,亲验)⇒ 本包内部臂用 `_sema_modelGate` / `_sema_autoMode`
2856
+ 前缀,不自铸 SDK 同名形。SDK 声明该臂之后改名是 **BREAKING**,另立一批。
2857
+ - **纯投影口刻意不导出**:`wiringManifestSupersetBody` 的入参是未校验 wire 帧,唯一诚实的声明形
2858
+ 是 `Record<string, unknown>` —— 推上公面等于在包公面开一个无类型边界(typeshape 棘轮正是为此
2859
+ 存在)。端要消费走**已定型**的 chrome 臂;两个具名形 `WiringManifestModelGate` /
2860
+ `WiringManifestAutoMode` 照常导出。
2861
+ - **`autoMode` 只在有 run 的时候来**:它骑在 per-leg 的 live 帧上。**一次性诊断进程**(没有任何
2862
+ 引擎腿)读不到它 —— 那种面上只能诚实报「本进程未观测到」,**不许**折成 `armed:false`。
2863
+
2864
+ **实现锚**:`src/adapter/downstream/eventToSdkMessage.ts`(`case 'wiring_manifest'` +
2865
+ `projectModelGateSection` / `projectAutoModeSection` / `wiringManifestSupersetBody`)、
2866
+ `src/adapt/arms.ts`(`wiringManifestArm`)、`src/seam.ts`(`WiringManifestChromeEvent` +
2867
+ `CHROME_ARM_TABLE` 加员)。
2868
+ **常驻门**:`scripts/run-type-superset-ledger-test.mjs`(两个 `_sema_` 键登记)、
2869
+ `scripts/run-integration-doc-freshness-test.mjs` ③ §3c(臂表双向对账)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@sema-agent/agent-types": "^0.2.0",
38
- "@sema-agent/core": "^7.1.0",
38
+ "@sema-agent/core": "~7.3.0",
39
39
  "@sema-agent/sdk": "^7.4.0",
40
40
  "esbuild": "^0.27.4",
41
41
  "typescript": "^6.0.2"