@sema-agent/client-core 0.53.0 → 0.55.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
@@ -13,7 +13,7 @@
13
13
  > 不在账上,都红。**发包批的义务** = 往 `FROZEN` **追加**一行(版本号 + 发布 commit + 段 sha256),
14
14
  > **绝不改写既有行** —— 既有行是「那一版当时发了什么」的历史,不是可变配置。
15
15
  >
16
- > 🔴 **阶段一段头义务(0.48.0 勘误后写死,防第三例)**:发布 commit(阶段一)必须**同批**把
16
+ > 🔴 **阶段一段头义务(0.48.0 勘误后写死;0.53.0 第三例后由 ④a0 机械执行)**:发布 commit(阶段一)必须**同批**把
17
17
  > `## <version>(未发布)` 段头转成日期形 `## <version>(YYYY-MM-DD)`(0.47.0 先例)。发布后该段
18
18
  > 字节即被 ④ 门逐字锁定,漏转**不可回改**、只能永久勘误 —— 0.36.0 与 0.48.0 两条勘误的共同根因
19
19
  > 就是这一步没做;此前头注只写了「未发的进 `(未发布)` 段」而没写「发布时转正」,歧义已由本句消除。
@@ -39,6 +39,173 @@
39
39
  > publish 后才发现。该段字节已被 ④a 逐字锁定在 `4fae01b` 上 ⇒ 标题**不回改、且永远不会被改**;
40
40
  > 门侧窄豁免同批登记(`KNOWN_HEADING_ERRATA` `version: '0.48.0', releasedAt: '4fae01b'`),
41
41
  > 与本段互钉,均为永久记录。
42
+ >
43
+ > 🔴 **勘误(0.53.0 发包批,2026-09-04,永久记录 —— 同案第三例,机理同上)**:`## 0.53.0` 段标题的
44
+ > `(未发布)` 是发布批的操作失误 —— 0.53.0 **已经发布**(releasedAt=npm gitHead `2a159e3`,registry
45
+ > 在架,dist.shasum eb59fb2c…),阶段一 commit 应当把段头转正为日期形而没转(施工车按「本批不发包」
46
+ > 写了 `(未发布)`,主收货把「发布即钉死字节」这一格漏了)。该段字节已被 ④a 逐字锁定在 `2a159e3` 上 ⇒
47
+ > 标题**不回改、且永远不会被改**;门侧窄豁免同批登记(`KNOWN_HEADING_ERRATA` `version: '0.53.0',
48
+ > releasedAt: '2a159e3'`),与本段互钉,均为永久记录。**第三例之后的根治**:头注义务只是文字,三次都没
49
+ > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
+ > commit 漏转在发布前就红,不再靠人记。
51
+
52
+ ## 0.55.0(2026-09-05)
53
+
54
+ ### `waiting_first_token` —— 等第一个 token 是一种**状态**,不是一次错误(core 7.3.0 / #530;cli B-003 / L-108①)
55
+
56
+ 用户报案(sema-bug4「模型调用失败对用户不可见」)的两个面,同一个根因:
57
+
58
+ | 引擎 | 用户看到 |
59
+ |---|---|
60
+ | 7.57.0(内嵌 core 7.2.0) | **什么都没有** —— 相不存在,壳无从渲 |
61
+ | 7.58.0(内嵌 core 7.3.1) | 一条**错误色**的 `API error` 行 —— 方向反了:一次尝试都没失败过 |
62
+
63
+ core 7.3.0 在 `BrainStatusPhase` 上加了第七相 `waiting_first_token`:首 token 看门狗已武装
64
+ (`firstTokenTimeoutMs > 30_000`)而第一个字还没到,等满 30s 起每 30s 发一帧,携 `elapsedMs` /
65
+ `timeoutMs`(`core/dist/brain/stream-engine.js` 的 `emitWaitFrame` 铸点)。本包 0.54.0 及以前
66
+ `mapBrainStatusToRetry` 的 `switch` 只认六相 ⇒ **第七相落 `default:` 被投成 `{kind:'error'}`**,
67
+ 壳照 `kind` 渲成重试行。病形 = 「上游闭集加员落 default 臂 ⇒ 新状态被渲成最像的旧状态」,
68
+ 与 #3004 修的 `recovered`(重试**成功**被渲成 `API error`)逐字同族。
69
+
70
+ - **新 `kind: 'waiting'` 臂**(`src/retryStatus.ts`):携 `elapsedMs` / `timeoutMs`,不打 `terminal`
71
+ 位(首 token 到了 core 补 `recovered`,彻底失败补 `gave_up` —— 等待本身从来不是终态)。
72
+ `deadline` 恒 = 映射时刻的 `nowMs`(本相不带 `retryIn*`)⇒ 渲染面按「剩余量 ≤ 0」自然不渲倒计时。
73
+ - 🔴 **老宿主分两形(不许含混说「零差异」)**:`waiting` 臂刻意是 `error` 臂的**结构超集** ——
74
+ **结构形**消费者(`if (kind==='stalled') … else …`)逐字节零差异;而**穷尽形**消费者
75
+ (`switch (kind)` + `default: assertNever`)**加员即破**(重新编译报「waiting 不可赋给 never」,
76
+ 已编译老产物在 default 臂**抛**)—— 破的是判别值本身,补结构位救不了。⇒ 对这一类消费方
77
+ 0.55.0 是 **BREAKING**,三端必须审一遍自己的 `RetryStatus` switch。两形的事实分别钉在
78
+ `run-client-core-pure-test.mjs` G6-e ①②③。结构形的细节:还没跟车的端
79
+ (desktop / web-client)其 `if (kind==='stalled') … else …` 的 else 臂照旧读到 `error` /
80
+ `deadline` / `terminal`,渲出与 0.54.0 **逐字节相同**的那一行。少了 `error` 这一位,
81
+ `status.error.rateLimits` 就是一次 TypeError —— 渲染路径裸抛 = 整屏崩,把「一行渲错」升级成
82
+ 「一屏全没」。判据不是一句承诺,是门:`run-client-core-pure-test.mjs` G6-d 与
83
+ `run-additive-key-passthrough-test.mjs` G5d 拿**本函数自己的 `default:` 臂**当活对照
84
+ (一个未知相走的正是加员前第七相走的那条臂),两边逐位相等才绿。
85
+ - **两位跟车过两层白名单**:`BrainStatus` 加 `elapsedMs` / `timeoutMs`(九键 → **十一键**)。
86
+ 🔴 族扫落点是**两处**,不是派工单点名的那一处:`adapter/downstream/eventToSdkMessage.ts`
87
+ 的 `case 'status'`(wire → 内部臂)与 `adapt/arms.ts` 的 `retryStatusArm`(内部臂 → chrome 事件)。
88
+ 漏任一层,新相到壳只剩空壳:kind 对了、秒数永远缺席。两位缺席一律**键不 stamp**,绝不折 0。
89
+ - **相位运行期镜像**六相 → **七相**;devDep `@sema-agent/core` `^7.1.0` → `~7.3.0`
90
+ (对账物不抬,`engine-vocab` G2-b 对新相**没有判别力** —— 这正是该门头注记过五次的恒绿病根)。
91
+
92
+ ### 抬对账物同批捞出的三件存量(族扫,不是点名件)
93
+
94
+ 抬 devDep core 到 7.3.1 后 `engine-vocab` 门当场翻红三处,逐条如实修:
95
+
96
+ 1. **`CC_DETAIL_TYPES` 漏 `list-agents`**(42 / core 43)—— ListAgents 的结构化卡
97
+ (`core/dist/agents/list-agents-tool.js` 铸点)。漏词的实际后果是那一类卡**永远退回正则解
98
+ 模型面文本**,而任何一层都不会响。与 5.20 / 5.43 两次逐字同族。
99
+ 2. **`EditedRuleTextPrecheck` ok 臂漏 `warnings`**(core 7.3.0 / design/382 §3.3-5 的**宽度提示**)。
100
+ 这条腿是端口注入(值原样穿过),所以漏的不是运行期的值,是**消费端的类型面可见性** ——
101
+ 端读不到就不会渲,「宽度在点头之前被说出来」就只剩引擎那半边。同批转出
102
+ `EditedRuleBreadthWarning`(`code` 按本包一贯纪律取**开集串**,不镜像 core 的闭集枚举)。
103
+ 3. **门自身的抽取口径病**:`extractStringUnion` 的 `([^;]+);` 在**第一个分号**处截断,而 core 7.3.0
104
+ 的 `BrainStatusPhase` 成员之间夹着一段**正文带分号**的 JSDoc ⇒ 第七相被静默漏抽,门报出
105
+ 「core 6 相 / 本包 7 相;多:waiting_first_token」这种**方向反了的**红 —— 一个抽取口径的病
106
+ 足以把跟车做对的人判成做错。修 = 先剥注释再匹配(同族的 `extractInterfaceKeys` 早就这么做),
107
+ 并补两条自证腿(带分号 JSDoc 夹层 / 剥注释后不把注释里的串当成员)。
108
+
109
+ ### 已知局限(本版新增)
110
+
111
+ - 壳半场的文案是 sema 超集:CC 2.1.250 语料**无同形提示行**(`cli250.js` 里 `first token` /
112
+ `firstToken` / `first_token` 的全部命中都是账号首 token 日期与流式计时**遥测**,无渲染面;
113
+ 同族里唯一的用户可见行是 `Waiting for API response`,那是 `reconnecting` 相的既有行)。
114
+ ⇒ 措辞按 [6386] / L-79② 既定形,登记 divergence。
115
+ - 未跟车的**结构形**端(desktop / web-client)在 7.58.0 引擎上仍会渲 0.54.0 那一行。这是加员的
116
+ **设计选择**而不是遗漏:零崩渲、零行为漂移,跟车与否由各端自己排期。**穷尽形**端不在此列 ——
117
+ 见上一条,那是 BREAKING,必须加 case。
118
+ - 🔴 **本版发出去也还不能让用户看见这一行**(异源对抗复审 [high] 如实记):cli 侧 deps 仍是
119
+ `@sema-agent/client-core ^0.53.0` / `@sema-agent/core ^7.2.0`,ENGINE_PIN 仍是 server 7.57.0
120
+ (内嵌 core 7.2.0,**没有**这个相)。⇒ 端到端可见需要三件同批:0.55.0 发布 → cli deps 抬到
121
+ ^0.55.0(含 lockfile)→ ENGINE_PIN 抬到 ≥7.58.0,并在**那一对真版本**上重跑 live 腿、
122
+ 摘掉它在 `skip-baseline.json` 里的必 SKIP 登记。
123
+
124
+ - **发包前异源扫描四修(2026-09-05,三域 opus 扫描 + opus 反证,17 条 7 实证)**:①[medium] **waiting 相的覆盖层拆除生命周期无门**——
125
+ 把 `retryStatusArm` 尾行 `noteRetryOverlay()` 对 waiting 摘掉,pure 3455 + additive 81 照样全绿 ⇒ 补 R1w 三判据(waiting 后首个主 lane
126
+ 内容帧即 `status:null`、先于 `response_metrics start`、子流帧不清);②[low] `elapsedMs`/`timeoutMs` 负数/NaN 直穿 ⇒ `saneMs` 只认有限非负
127
+ 数(缺席不渲 0);③[medium] cli 侧 waiting 相曾走 RetryStatusRow 的 all-or-nothing 槽,整块顶掉动画 glyph 与每秒计时,30s 一帧之间屏上零变化
128
+ (像卡死)⇒ 壳改为只替换 byline、glyph 照常动(cli 半场);④文档/门勘误:`retryStatus.ts` 键集注释 9→11 键、server 7.54.0→7.58.0;typeshape
129
+ 棘轮 b4 21→20 的推导理由改按实测(此前只有一处内联);engine-vocab `CORE_FLOOR` 7.1→7.3 与对账物同步;INTEGRATION-CLIENTS §0a 已发版本的
130
+ 「未发」告示按「发出即退场」约定退场。**老宿主三形**(§18c 更正):结构形(读 `kind` 走 default)零差异;穷尽形(`switch`+`assertNever`)
131
+ BREAKING;**不读 kind 的宿主**(web-client 现状)在 waiting 相上仍渲既有错误行「Request failed · retrying」——不是回归(0.54.0 同形),
132
+ 但接 §18b 才有真进度行。
133
+
134
+ ## 0.54.0(2026-09-05)
135
+
136
+ ### design/385 —— 三条引擎注入车道的类型化投影(`src/peerFrames.ts`;cli L-61② / L-87 5a①5a②)
137
+
138
+ 引擎把三类**根本不是同一种东西**的载荷塞进同一条 `task_notification` 车道,而 core 对这三类
139
+ **不套** `<task-notification>` 壳(`renderTaskNotificationXml` 头三个分支):
140
+
141
+ | 载体键 | 出处 | 模型面 |
142
+ |---|---|---|
143
+ | `agentMessage` | §1.4 d1,同进程子代 `SendMessage("main")` uplink | `<agent-message from="…">` |
144
+ | `crossSessionMessage` | §4.1,另一个会话的消息从本会话信箱 drain | `<cross-session-message from="…" …>` |
145
+ | `crossSessionNotice` | §4.4/§5.2,**本会话自己发出去**那条消息的回执 / idle 通知 | 一行纯文本 |
146
+
147
+ 端此前照泛化卡渲 ⇒ 用户看到「后台任务完成」卡,而模型读到的是一条同事发来的话:**两个面说的
148
+ 不是同一件事**。本批把判定与编解码整只收进包:
149
+
150
+ - `classifyPeerNotification(rawPayload)` —— 三分投影,认不出返 `null`(= 泛化卡,调用方零改动)。
151
+ 🔴 判别位是**类型化载体的在场**,永远不是 `summary` 文本:三条载体只有引擎注入腿铸得出
152
+ (`ExternalNotificationInput` 是 `TaskNotificationPayload` 的真子集,外部 `notify()` 一个都穿不上),
153
+ 而 `summary`/`result` 是任何一条通知都填的字段 —— 锚文本 = 任何后台任务把
154
+ `<agent-message from="…">` 写进 summary 就能冒充一条同事消息。
155
+ - **fail-closed 分两档**:必填位(`from` 空 / `body` 非串 / notice `kind` 不在闭集)⇒ 整帧退泛化卡
156
+ (诚实降级,用户仍看得见);可选位形不合(`fromMode` 写了闭集外的词)⇒ **只丢那一位**。
157
+ - `_sema_provenance` **必须在场且 `kind` 与车道相符**(异源对抗复审 [high] 翻面):首版写的
158
+ 「缺席放行」理由是「要求在场会让另两条车道在老引擎上恒死」,**该理由被证伪** —— 老引擎上那两条
159
+ 车道连载体键都没有、判定根本走不到,而 core 契约明写 provenance「present exactly when the carrier
160
+ is」,在跑的 7.2.0 就是同一处同时铸两者。要求在场对真载荷零代价,对畸形 / 半截注入 / 版本漂移则
161
+ 关上一道门(否则只凭 `from`+`body` 两个字符串就能让屏上出现一张署着任意名字的消息卡)。
162
+ 判据是**四条等式 + 在场**(`kind` + `from`/`taskId`/`seq` 三位**必须在场**且与载体、载荷逐一相等
163
+ —— core 的 `SemaProvenance` 把这三位全声明为必填,三个铸点也都同址写下,所以「两侧都缺」不是
164
+ 合法兼容形而是半截载荷;首版只写 `a === b`,`undefined === undefined` 正好把它放行了):只核 `kind` 拦得住
165
+ 半截载荷,拦不住**同 kind 的伪造** —— 载体署一个可信名字、provenance 三位全对不上,屏上照样出现
166
+ 一条署着那个名字的消息。后三条各有**铸点直证**(uplink 腿与跨会话 drain 腿都在同一个对象字面量里
167
+ 同值写下 `task_id`/`seq`/`from`),铸点一改由 cli 侧 wire 锚 A-K24 的同址探针当场喊红。
168
+ ⚠️ 载荷那一边是 snake `task_id`、provenance 那一边是 camel `taskId` —— 同一个量两个拼法,写混了
169
+ 等式恒不成立、整条车道静默死掉(本模块首版真写混过一次,当场被门抓住)。
170
+ - **通知臂的分支序修正**:三车道判定挪到**完成卡台账早退之前**。`isWorkflowCompletionCardEnqueued`
171
+ 回答的是「这个 run 的**完成卡**是不是已经入过队」,与「这一帧是不是一条同事消息」无关;而子代
172
+ **续跑**时复用同一个 taskId,它的合法 `agentMessage` 帧就会在那道早退上被整条丢掉(零转录输出,
173
+ 且渲染去重账已记过 ⇒ 重放也补不回来)。peer 帧豁免该早退;泛化完成通知照旧被它吞掉(反向钉在门里)。**同因第二件**:peer 帧也不再写
174
+ `markEngineWorkflowNotified` 那本「完成已通知」的跨通道账 —— 一条同事**消息**不是完成事件
175
+ (三条 peer 铸点的 `status` 都是 `"event"`),写脏了这本账,子代自己真正的空闲期完成通知随后会被
176
+ 补发通道整条丢掉(修前实测:同 taskId 的补发入队恒 0)。
177
+ - **拆火编码是单射**:`escapeEnvelopeTag` 连正文里**原本就有**的反斜杠一起数(`<` 后跟任意多个
178
+ 反斜杠再跟标签名时都插一个),`unescape(escape(x)) === x` 对一切 x 成立。core 那一份是**单向**
179
+ 消毒(模型读完就结束、没有反向腿),照抄到这里就是一个非单射映射:合法代码文本 `<\agent-message>`
180
+ 会在往返之后被改写成真标签,既损坏正文又是一条注入路。
181
+ - **解析腿三条硬边界**:信封必须整串锚定;正文里出现**未拆火**的同名标签 ⇒ 整条退 `null`
182
+ —— 贪婪匹配会把「两封拼一起」读成一封(正文合并、两位署名全记第一位头上)、把「信封 + 尾随
183
+ 文本」读成一封(尾随文本静默消失)。退 `null` = 落回普通文本行,一个字节都不丢。
184
+ - 优先序**照抄 core 渲染腿**(agentMessage → crossSessionMessage → crossSessionNotice):模型那一面
185
+ 已经按这个顺序读过了,端另立一套顺序 ⇒ 屏上的卡与模型读到的帧对不上。
186
+ - `renderPeerFrameTranscriptText` / `parsePeerFrameText` 两半**同源在包**:端的消息面(CC 血统:
187
+ 按标签分派)只拿得到文本,两半不同源就会有人在端里再抄一份正则 —— cli-191 A-D1/A-D12 那一族的
188
+ 成因就是这个。通知车道**刻意不进解析腿**(锚 `[Cross-session …]` 前缀会把用户随手打的同样一行
189
+ 认成引擎通知),它本来就该按普通文本行渲(CC 形)。
190
+ - `peerFrameDisplayName` —— 「@谁」的优先序(跨会话 `fromName` > `from`)也在包里,端不许自己排。
191
+ - **发包前异源扫描三修(2026-09-05,三域 opus 扫描 + opus 反证,12 条 11 实证)**:①[high] 正文**权威信封标签拆火**
192
+ ——渲染腿此前只拆同名信封,漏了 core `neutralizePeerBody` 那一步:子代正文里一段 `<task-notification>…<status>completed</status>…`
193
+ 会原样进转录 block,宿主 resume 时 `parseTranscriptNotificationSeeds` 把它读成真完成通知 ⇒ 去重台账被毒化、受害 run
194
+ 的真完成通知被跨通道臂吞掉(dist 真跑实证);现按 core 同形对 8 个权威标签 `<` 后插 ZWSP,单向不还原;通知纯行同拆并拆两个
195
+ 消息信封标签(整体形如信封的通知不再被端认成消息)。②[high] 属性长度非对称——渲染无界、解析封顶(from 400 / from-name 200)
196
+ ⇒ 包自己铸的帧解析回 null、用户看到裸 XML;解析改无界(线性匹配)。③[medium] 署名规范化——`from`/`fromName` 含换行/控制符
197
+ 折成单行单空格,折空按缺席处置。文档同批勘误:README/门契约串/§17c 中「provenance 只否决不认证、缺席放行」为首版残句,
198
+ 实现自定稿起即为「在场 + 四等式」;§0a 版本锚与 §1c 闭包棘轮数字刷新。
199
+
200
+ **老宿主零差异**:三条载体一个都不在场的载荷(= 今天的全部载荷)走的分支与本批之前**逐字节等价**;
201
+ `taskNotificationArm` 只换了转录行那一行的文本来源,去重键 / settle / 面板 / 台账一个都没动
202
+ (它们判的是「哪个 task 的哪个状态」,与这条通知在模型面穿哪件外衣无关)。
203
+
204
+ **新增常驻门**:`scripts/run-peer-frame-projection-test.mjs`(71 checks,含**适配器级**的两条台账回归)。
205
+
206
+ **已知局限(本版新增)**:`crossSessionMessage` / `crossSessionNotice` 两条载体在 **core 7.2.0
207
+ (= ENGINE_PIN 7.57.0 内嵌那一版)上零铸点** —— 本批是消费半场先落地,上游到货前这两条车道恒
208
+ 不触发。`agentMessage` 一条今天就到得了(core 7.2.0 `agents/send-message-tool.js` 直证)。
42
209
 
43
210
  ## 0.53.0(未发布)
44
211
 
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.53.0
38
+ **Version:** 0.55.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
@@ -263,6 +263,7 @@ public-surface guard checks that last one).
263
263
  | `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `isApiErrorMessage` class flag so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry |
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
+ | `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" |
266
267
 
267
268
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
268
269
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -1,3 +1,4 @@
1
+ import { classifyPeerNotification, renderPeerFrameTranscriptText } from '../peerFrames.js';
1
2
  import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
2
3
  import { mapBrainStatusToRetry } from '../retryStatus.js';
3
4
  import { steeringInjectedToAttachments } from '../steering.js';
@@ -676,19 +677,46 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
676
677
  yield* panel.settleFromNotification(taskId, rawStatus !== 'completed');
677
678
  }
678
679
  }
680
+ // ── design/385:三条引擎注入车道的判定**必须在下面那道完成卡早退之前**做 ────────────────
681
+ // 🔴 异源对抗复审 r2 [high] 实测的吞消息形:`isWorkflowCompletionCardEnqueued(taskId)` 回答的是
682
+ // 「这个 run 的**完成卡**是不是已经入过队」,与「这一帧是不是一条同事消息」毫无关系。而一个
683
+ // 子代**续跑**时复用同一个 taskId(`_sema_provenance.taskId === task_id === senderId`,core
684
+ // 7.2.0 uplink 铸点直证),它的合法 `agentMessage` 帧就会在这道早退上被整条丢掉:零转录输出,
685
+ // 且渲染去重账已经记过 ⇒ 重放也补不回来。复现序:先 `noteWorkflowCompletionCardEnqueued(id)`,
686
+ // 再送同 id 的 uplink 帧 ⇒ 修前只剩 chrome 收尾事件、消息正文完全缺席。
687
+ // ⇒ peer 帧**豁免**这道早退(它不是完成卡,不参与完成卡的双通道去重)。
688
+ const peerFrame = classifyPeerNotification(n);
679
689
  // 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
680
690
  // 🔴 B5 ①:读的是 **module 台账**(cli `isWorkflowCompletionCardEnqueued` 逐字)——
681
691
  // 记账方(两个 enqueue + idle watcher)与清账方(上面的 drop)都在那儿。
682
- if (n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
692
+ if (peerFrame === null && n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
683
693
  return;
684
694
  // 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
685
695
  // 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
686
696
  // external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
687
- if (n.task_type !== 'external')
697
+ // 🔴 **peer 帧同样豁免**(异源对抗复审 r3 [high],实测:投一条合法 uplink 之后再给同 taskId 入队
698
+ // `{status:'completed'}`,队列恒 0、`bgCrossChannelDropped` +1)。这本账问的是「这个 run 的
699
+ // **完成**是不是已经被引擎注入过」——而一条同事**消息**不是完成事件(三条 peer 铸点的
700
+ // `status` 都是 `"event"`,core 直证)。子代跑到一半发了句话就把这本账写脏,它自己真正的
701
+ // 空闲期完成通知随后被补发通道整条丢掉,用户只能等下一次开流才可能看见。
702
+ if (peerFrame === null && n.task_type !== 'external')
688
703
  markEngineWorkflowNotified(taskId);
704
+ // 判定单源 = `classifyPeerNotification`(载体在场判别,绝不锚 summary 文本);认不出 ⇒ `null`
705
+ // ⇒ 这一行与本臂此前**逐字节等价**(三条载体一个都不在场的载荷 = 今天的全部载荷,老宿主零差异)。
706
+ // 🔴 除上面那道完成卡早退之外,其余账**全不动**:去重键 / settle / 面板 / 跨通道 mark 判的是
707
+ // 「哪个 task 的哪个状态」,与这条通知在模型面穿哪件外衣无关,跟着换会把几条独立的账搅成一本。
708
+ // (uplink 帧的 `status` 是 `"event"` —— core 7.2.0 铸点直证 ⇒ 终态那几条腿本来就不会对它开火。)
689
709
  yield transcript({
690
710
  type: 'user',
691
- message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
711
+ message: {
712
+ role: 'user',
713
+ content: [
714
+ {
715
+ type: 'text',
716
+ text: peerFrame !== null ? renderPeerFrameTranscriptText(peerFrame) : renderTaskNotificationXml(fields),
717
+ },
718
+ ],
719
+ },
692
720
  uuid: idOf(m),
693
721
  session_id: ctx.sessionId,
694
722
  }, ctx.now());
@@ -737,7 +765,7 @@ const retryStatusArm = function* (m, { ctx, flags }) {
737
765
  // #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
738
766
  if (typeof m.parentToolCallId === 'string')
739
767
  return;
740
- // 🔴 六键全转(#3004 跟修批 2026-08-08):此前这里只挑 phase/detail/retryInSec 三键,attempt /
768
+ // 🔴 十一键全转(0.55.0;首建 #3004 跟修批 2026-08-08):此前这里只挑 phase/detail/retryInSec 三键,attempt /
741
769
  // maxRetries / retryInMs 被剥在本层 —— 而 server 两腿的 brainStatusEventData 是全发的,
742
770
  // `RetryStatus` 上也早就留好了 attempt/maxRetries 两位。键集真源 =
743
771
  // `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对实装 core 对账)。
@@ -761,6 +789,14 @@ const retryStatusArm = function* (m, { ctx, flags }) {
761
789
  // 剥掉它 = 用户看得到「在重试」却看不到「谁失败了/该不该等」)。
762
790
  ...(typeof m.retryAtMs === 'number' ? { retryAtMs: m.retryAtMs } : {}),
763
791
  ...(typeof m.errorStatus === 'number' ? { errorStatus: m.errorStatus } : {}),
792
+ // ── 十一键(0.55.0;core 7.3.0 #530,server ≥7.58 `brainStatusEventData` 放行)──────────
793
+ // 🔴 **同一个病形的第五、第六例**,而且这一次剥掉的是**新相的全部内容**:
794
+ // `waiting_first_token` 帧上除了 phase 就只有这两位携带信息(它不带 retryIn*/attempt/
795
+ // errClass/errorStatus —— core 明说本相不宣告等待)。这层白名单不跟车,`mapBrainStatusToRetry`
796
+ // 的新 `waiting` 臂就只能收到一个空壳:kind 对了、秒数永远缺席,屏上那行渲不出「等了多久」。
797
+ // ⇒ 加相**必同批加键**;键集真源仍是实装 core 的 `BrainStatus` 整个键集(G2-c 逐键等值门)。
798
+ ...(typeof m.elapsedMs === 'number' ? { elapsedMs: m.elapsedMs } : {}),
799
+ ...(typeof m.timeoutMs === 'number' ? { timeoutMs: m.timeoutMs } : {}),
764
800
  }, ctx.now());
765
801
  // `status === null` = 引擎直报 `recovered`(重试成功)。这一拍是**清除腿之一** —— 但间接腿
766
802
  // (TurnFlags 的「主 lane 内容帧到达即清」)可能已经清过了:已安装 core 在内容流跑完的 finally
@@ -344,6 +344,11 @@ export function eventToSdkMessage(ev, ctx) {
344
344
  // 不等 SDK**,类型面缺席不等于 wire 上缺席。
345
345
  // 📋 **退役条件**:sdk 把这两位补进 `status` 臂声明的那一批,把它们并进上面的类型面直读、
346
346
  // 删掉本段的 `st2` 结构视图(与本批刚兑现的四键退役逐字同一条路)。
347
+ // 🔴 **0.55.0 又添两位,同一条退役路**(core 7.3.0 #530 / server ≥7.58 `brainStatusEventData`
348
+ // 放行):`elapsedMs`(第一个 token 已被等了多久)与 `timeoutMs`(这次等待的天花板)。
349
+ // 它们只在新相 `waiting_first_token` 上在场,而那一相**除了 phase 就只有这两位带信息** ——
350
+ // 本层剥掉 = 新相到了下游只剩一个空壳,屏上渲不出「等了多久」。sdk 的 `status` 臂同样尚未
351
+ // 声明这两位 ⇒ 与上面两位同款结构视图读,退役条件同(sdk 补声明的那一批一起并进类型面直读)。
347
352
  const st2 = ev;
348
353
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
349
354
  const attempt = num(ev.attempt);
@@ -351,6 +356,8 @@ export function eventToSdkMessage(ev, ctx) {
351
356
  const retryInMs = num(ev.retryInMs);
352
357
  const retryAtMs = num(st2.retryAtMs);
353
358
  const errorStatus = num(st2.errorStatus);
359
+ const elapsedMs = num(st2.elapsedMs);
360
+ const timeoutMs = num(st2.timeoutMs);
354
361
  // 非空串才透传(空串既不是桶也不是「不知道」,只会在下游被渲成一个空的原因)。
355
362
  const errClass = typeof ev.errClass === 'string' && ev.errClass.length > 0 ? ev.errClass : undefined;
356
363
  return projected(stamp(ctx, armBody({
@@ -370,6 +377,11 @@ export function eventToSdkMessage(ev, ctx) {
370
377
  // **全链**(adapt 入口 → chrome 事件),所以漏任一层它都红。
371
378
  ...(retryAtMs !== undefined ? { retryAtMs } : {}),
372
379
  ...(errorStatus !== undefined ? { errorStatus } : {}),
380
+ // ── 0.55.0 跟车两位(core 7.3.0 #530 / server ≥7.58 放行)──────────────────────────
381
+ // 族扫同址:本条腿的**两层白名单**(本层 + `adapt/arms.ts` 的 `retryStatusArm`)本批同改,
382
+ // 与上面 0.48.0 那两位逐字同一个理由;漏任一层,新相 `waiting_first_token` 的秒数就到不了壳。
383
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
384
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
373
385
  // 🔴 §E2 lane 身份必须透传(2026-08-08 对抗复审二轮复审命中的**跨 lane 状态破坏**)。
374
386
  // `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
375
387
  // `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
@@ -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 闸拒时缺席(开集串,见类型注)。 */
package/dist/index.d.ts CHANGED
@@ -135,6 +135,7 @@
135
135
  export * from './seam.js';
136
136
  export * from './workflow.js';
137
137
  export * from './notifications.js';
138
+ export * from './peerFrames.js';
138
139
  export * from './steering.js';
139
140
  export * from './diagnostics.js';
140
141
  export * from './retryStatus.js';
package/dist/index.js CHANGED
@@ -135,6 +135,13 @@
135
135
  export * from './seam.js';
136
136
  export * from './workflow.js';
137
137
  export * from './notifications.js';
138
+ // ── design/385(0.54.0):三条引擎注入车道的类型化投影 + 转录行文本编解码 ────────────────────
139
+ // 收在库里的理由:① 判别位是**类型化载体的在场**(agentMessage / crossSessionMessage /
140
+ // crossSessionNotice),端若各自判就会有人拿 `summary` 文本判 —— 那条路上任何后台任务都能把
141
+ // `<agent-message from="…">` 写进 summary 冒充一条同事消息;② 端的消息面只拿得到**文本**,
142
+ // 所以「帧→文本」与「文本→帧」两半必须同源在包里,端才做得到零字符串判定(cli-191 A-D1/A-D12
143
+ // 那一族的成因正是消费端各自抄一份判据)。
144
+ export * from './peerFrames.js';
138
145
  export * from './steering.js';
139
146
  export * from './diagnostics.js';
140
147
  export * from './retryStatus.js';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * peerFrames.ts — design/385 的三条**引擎注入帧**在客户端的类型化投影(判定单源在包,端只装配)。
3
+ *
4
+ * ── 这一件守的是什么 ────────────────────────────────────────────────────────────────
5
+ * 引擎把三类东西塞进 **同一条** `task_notification` 车道,而它们在模型面根本不是同一种东西
6
+ * (core `renderTaskNotificationXml` 对这三类**不套** `<task-notification>` 壳):
7
+ * · `agentMessage` 同进程子代 → 父会话的 uplink(`SendMessage("main")`,§1.4 d1);
8
+ * · `crossSessionMessage` 另一个会话的消息,从本会话自己的信箱里 drain 出来(§4.1);
9
+ * · `crossSessionNotice` 关于**本会话自己发出去的**那条消息的回执 / idle 通知(§4.4 / §5.2)。
10
+ * 端如果照 `task_notification` 泛化卡去渲,用户看到的是一张「后台任务完成」卡,而模型读到的是
11
+ * 一条同事发来的话 —— 两个面说的不是同一件事。
12
+ *
13
+ * 🔴 **判别位是类型化载体的在场,永远不是 `summary` 文本**。这三条载体只有引擎的注入腿铸得出
14
+ * (`ExternalNotificationInput` 是 `TaskNotificationPayload` 的**真子集**,外部 `notify()` 一个
15
+ * 都穿不上);而 `summary`/`result` 是任何一条通知都填的字段,拿它做判据 = 任何一个后台任务
16
+ * 只要把 `<agent-message from="…">` 写进 summary 就能冒充一条同事消息。
17
+ *
18
+ * 🔴 **身份三条等式**:`_sema_provenance` 必须在场、`kind` 与车道相符,且 `from`/`taskId`/`seq`
19
+ * 与载体和载荷逐一相等(三条都由 core 铸点直证,见 `provenanceAgrees`)。只核 `kind` 拦得住
20
+ * 半截载荷,拦不住**同 kind 的伪造**——载体署一个可信名字、provenance 三位全不对,屏上照样
21
+ * 出现一条署着那个名字的消息。
22
+ *
23
+ * 🔴 **fail-closed**:载体在场但形不合(`from` 空、`body` 非串、notice 的 `kind` 不在闭集里)⇒
24
+ * 返回 `null` = 退回泛化卡。退回泛化卡是**诚实降级**(用户仍看得见这条通知,只是没有专用形);
25
+ * 而放一个半截形出去,端就会拿 `undefined` 去渲一张署名为空的「来自 @undefined 的消息」卡。
26
+ *
27
+ * 🔴 **优先序照抄 core 的渲染腿**(`agentMessage` → `crossSessionMessage` → `crossSessionNotice`)。
28
+ * 多载体同时在场是矛盾载荷,但**模型那一面已经按 core 的顺序读过了** —— 端按同一个顺序判,
29
+ * 两个面才说同一件事;这里另立一套「矛盾就退泛化」会让屏上那张卡与模型读到的帧对不上。
30
+ *
31
+ * ── 为什么渲染/解析两半都在包里 ─────────────────────────────────────────────────────
32
+ * 端的消息面(CC 血统:`UserTextMessage` 按标签分派)拿到的只有**文本**,没有帧。所以包必须同时
33
+ * 给出「帧 → 文本」与「文本 → 帧」两半,且**同源**(同一组标签常量、同一套属性序),端才可能
34
+ * 做到零字符串判定。两半的往返在常驻门里逐形对拍。
35
+ *
36
+ * ── 与上游的锚 ──────────────────────────────────────────────────────────────────────
37
+ * 两个标签字面量是 core 的铸点镜像(`core/task-notification.js::AGENT_MESSAGE_TAG` /
38
+ * `agents/cross-session-envelope.js::CROSS_SESSION_MESSAGE_TAG`),登记在壳的 wire 锚契约表里
39
+ * (cli `scripts/wire-anchor-registry.mjs`)—— core 改词当天那道门就红,而不是等到用户发现
40
+ * 「同事消息又变回一张后台任务卡了」。
41
+ */
42
+ /** core `core/task-notification.ts::AGENT_MESSAGE_TAG` 的镜像(同进程 uplink 的模型面标签)。 */
43
+ export declare const AGENT_MESSAGE_TAG = "agent-message";
44
+ /** core `agents/cross-session-envelope.ts::CROSS_SESSION_MESSAGE_TAG` 的镜像(跨会话信封标签)。 */
45
+ export declare const CROSS_SESSION_MESSAGE_TAG = "cross-session-message";
46
+ /** core `_sema_provenance.kind` 的闭集(7.4.0 三员;7.2.0 只有第一员 —— 缺员按「上游还没发」处理,
47
+ * 不是「上游改词」:本模块从不要求 provenance 在场,只要求**在场时不自相矛盾**)。 */
48
+ export declare const PEER_FRAME_LANES: readonly ["agent_message", "cross_session_message", "cross_session_notice"];
49
+ export type PeerFrameLane = (typeof PEER_FRAME_LANES)[number];
50
+ /** core `crossSessionNotice.kind` 的闭集(§4.4:投递回执 / idle 回执两形)。 */
51
+ export declare const CROSS_SESSION_NOTICE_KINDS: readonly ["delivery_notice", "idle_notice"];
52
+ export type CrossSessionNoticeKind = (typeof CROSS_SESSION_NOTICE_KINDS)[number];
53
+ /** core `PermissionModeClass`(发送方**自述**的权限模式类,不是被证实的事实)。 */
54
+ export declare const PEER_MODE_CLASSES: readonly ["bypass", "prompting"];
55
+ export type PeerModeClass = (typeof PEER_MODE_CLASSES)[number];
56
+ export type AgentMessageFrame = {
57
+ lane: 'agent_message';
58
+ /** 帧的 `from="…"` 属性所拼的那个署名(子代的名字 / 标签)。 */
59
+ from: string;
60
+ body: string;
61
+ agentType?: string;
62
+ /** 生产者的 per-frame 计数器(= 本 payload 的 `seq`);缺席不补 0。 */
63
+ seq?: number;
64
+ };
65
+ export type CrossSessionMessageFrame = {
66
+ lane: 'cross_session_message';
67
+ /** 发送方的**地址**(可回信的那一个;§4.1 provenance 的 `from` 同值)。 */
68
+ from: string;
69
+ body: string;
70
+ fromSession?: string;
71
+ fromName?: string;
72
+ fromMode?: PeerModeClass;
73
+ fromScope?: string;
74
+ seq?: number;
75
+ };
76
+ export type CrossSessionNoticeFrame = {
77
+ lane: 'cross_session_notice';
78
+ kind: CrossSessionNoticeKind;
79
+ /** 引擎自铸的整行文本(`[Cross-session delivery notice] …` / `[Cross-session idle notice] …`)。
80
+ * 🔴 端**原样渲一行**:这行是引擎按类型化 `peerMeta.notice` 铸的,端再解析一遍就是拿散文当数据。 */
81
+ text: string;
82
+ };
83
+ export type PeerFrameProjection = AgentMessageFrame | CrossSessionMessageFrame | CrossSessionNoticeFrame;
84
+ /**
85
+ * `task_notification` 原始 wire 载荷 → 三条引擎注入车道之一;认不出 ⇒ `null`(= 泛化通知,
86
+ * 调用方照旧走 `normalizeTaskNotification` / `renderTaskNotificationXml`)。
87
+ *
88
+ * 入参是**原始**载荷(snake_case `task_id` 那一份),不是 `normalizeTaskNotification` 的产物 ——
89
+ * 后者是一张 15 键白名单,这三条载体一个都不在里面。
90
+ */
91
+ export declare function classifyPeerNotification(n: Record<string, unknown>): PeerFrameProjection | null;
92
+ /**
93
+ * core `ENGINE_AUTHORITY_ENVELOPE_TAGS` 的镜像(`core/untrusted-text.ts::ENGINE_ENVELOPES` 里 kind="authority" 的标签):
94
+ * 这些信封在模型面/转录面代表**引擎权威**,一段同事正文里出现它们就是伪造。core 的 `neutralizePeerBody`
95
+ * (= `sanitizeUntrustedText(body, PEER_BODY_ENVELOPE_TAGS)`)在渲染信封**之前**先把它们拆火(`<` 后插 ZWSP),
96
+ * 本模块首版只抄了后一步(同名信封拆火)——异源发包扫描 [high] 实证:子代正文里一段
97
+ * `<task-notification><task-id>victim</task-id><status>completed</status>…` 会原样进转录 block,
98
+ * 宿主 resume 时 `parseTranscriptNotificationSeeds` 把它读成真完成通知 ⇒ 去重台账被毒化,受害 run 的
99
+ * 真完成通知随后被跨通道臂整条吞掉。这里补上那一步,并与 core 同形(`<\s*\/?\s*(tag)(\s[^>]*)?>` 全大小写)。
100
+ * 🔴 单向(与 core 一致):端的解析腿**不**还原 ZWSP —— 拆掉的权威标签就该永远是拆掉的。
101
+ */
102
+ export declare const AUTHORITY_ENVELOPE_TAGS: readonly ["system-reminder", "task-notification", "new-diagnostics", "user_memory", "scope", "skills", "total_tokens", "instruction-files"];
103
+ /**
104
+ * 投影 → 转录行文本(端的消息面按标签分派的那一份)。
105
+ *
106
+ * 🔴 这**不是** core 模型面信封的逐字副本,也不该是:core 那一份是给**模型**读的(带纪律块、
107
+ * 带 CC 逐字属性语法、带 round-trip 拒收);这一份是**端的转录回显**,只需要与本模块的解析腿
108
+ * 往返一致。刻意不复刻 core 的语法契约 —— 复刻一份对不上的副本比不复刻更坏
109
+ * (它会让人以为端这一行是模型读到的那一行)。属性序仍照 core 的规范序排,便于人肉比对。
110
+ */
111
+ export declare function renderPeerFrameTranscriptText(p: PeerFrameProjection): string;
112
+ export declare function parsePeerFrameText(text: string): AgentMessageFrame | CrossSessionMessageFrame | null;
113
+ /**
114
+ * 端渲「@谁」时该用的那个名字 —— 判定在包,端不许自己排优先序。
115
+ * 跨会话:`fromName`(人取的名)优先于 `from`(地址);同进程:只有 `from`。
116
+ */
117
+ export declare function peerFrameDisplayName(p: AgentMessageFrame | CrossSessionMessageFrame): string;