@sema-agent/client-core 0.44.0 → 0.46.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
@@ -28,6 +28,520 @@
28
28
  > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
29
  > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
30
30
 
31
+ ## 0.46.0(2026-08-30)
32
+
33
+ 两件修复批(#354/#355 双 P0 现网案的 client-core 半场):
34
+
35
+ - **`isResumeAtRejection` 文本兼容腿**(#355 D1,`4fa8617`):server 预检腿的 codeless 4xx 把
36
+ `resume_at` 真子码降级进人话文本,机读前缀腿判 false ⇒ #166 去锚自动重发整臂跳过 ⇒ 1.0.93
37
+ 现网会话永久死锁。新增文本腿只认闭集两对「状态 × 括号收尾子码」(`404×(resume_at.unknown_event)`
38
+ / `422×(resume_at.no_session)`),消费点 `endsWith` 串尾锚(闭集+串尾双件堵回显注入);
39
+ `message`/`errorMessage` 双载体各自独立判。**过渡垫片**:server 已在发端补结构化 `code`
40
+ (46e6e94,随 7.51;[5445] 承诺两句括号尾文案不动),最低 server 版本推过 7.51 后本腿整条退役
41
+ ——退役义务在此登记。
42
+ - **Bash 良性非零退出注记三臂消费**(#354 渲染案,`fc5eb5b`):引擎对 grep/rg 无命中族显式给
43
+ `isError:false` + `structured.returnCodeInterpretation`,而 `wireOutputToBody`(T11)/
44
+ `structuredToToolUseResult`/`printToolResultFrame` 三处错误位派生只看 `exitCode !== 0`,注记
45
+ 整族被丢 ⇒ 良性形渲红「Error: Exit code 1」。三臂同判别量收敛:注记在场 ⇒ 非零退出不翻错+
46
+ 注记过境 `toolUseResult`(宿主卡零输出分支可渲「No matches found」);无注记非零照翻([1948]
47
+ 裁定不动)。新门 `run-bash-benign-exit-interpretation-test.mjs`(11 checks)。
48
+
49
+ 已知局限(本版新增):无。
50
+
51
+ ## 0.45.0(2026-08-29)
52
+
53
+ **批面**:①design/285 **批 0-3 全四批** —— fleet 会话锚 per-key 化(在册局限 **P-31** 的正位解,🟡 部分闭合),
54
+ 从开流参数 → 归属判据 → owner 台账 → **读侧出站**整条贯通;②#323 / core #447 `text_end`(assistant 流式
55
+ 散文**段边界**)投影臂。
56
+ 公开面全 additive;🔴 **行为改变两处**:keyed 车道的 `ownByRoot` 放行集(批 1),
57
+ 以及 **tail / subagentOutput / taskOutput / taskStop 四条读面的 `?session=`(批 3,默认槽也变)**。
58
+
59
+ ### ① `engineSessionParamFor(sessionKey)`(新导出,批 0)
60
+
61
+ `?session=` 派生的 **per-key** 形 = `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一
62
+ (恒不返回空串:要么真值要么 `undefined`)。零参 `engineSessionParam()` 收敛成
63
+ `engineSessionParamFor(DEFAULT_SESSION_KEY)` 的兼容层 —— **同一条取值链,不抄第二份**,默认槽行为逐字不变。
64
+
65
+ ### ② per-stream ingress + epoch(批 1;keyed 开流参数与帧入口的唯一正身)
66
+
67
+ `FleetLedger` 新增 `issueStream()` / `issueSnapshot(opts?)`,各发一只 `FleetIngress`:
68
+
69
+ - **不可变捕获**:`{sessionKey, session, epoch}` 在**开流那一刻**解析一次,之后永不重读
70
+ (与 owner 台账 tick 腿「开流时一次性捕获、不逐帧现读」同一条纪律);
71
+ - **帧只能经 ingress 进入 ledger** ⇒「这条流是替谁开的」不再是宿主的一句声明,而是帧真正走过的通道;
72
+ - **重连 = 新 ingress + epoch+1**,旧 ingress 上的迟到帧丢弃并计数 —— 结构性堵死「旧流的帧配上新会话」
73
+ 这一错组合;`close()` 之后同理;
74
+ - **snapshot 独立 ingress**,绝不改写正在跑的 stream 锚与 epoch。
75
+
76
+ 🔴 **刻意不发** `fleetStreamOptionsFor(key)` / `fleetSnapshotOptionsFor(key)`:一个 module 级 keyed helper
77
+ 与 ledger 的绑定状态**互不可见**,会造出「用 B 参数开了 B 流而 ledger 显示未绑定」与「调了 helper 却把
78
+ 返回值丢掉而 ledger 显示已绑定」两条互斥假象 —— 它测的是「你调了哪个 helper」,不是「流按哪个 key 开」。
79
+ 零参 `fleetStreamOptions()` / `fleetSnapshotOptions()` **原样保留、语义一字不动**(对单会话默认槽宿主正确)。
80
+
81
+ `FleetLedgerStatus` additive 三键:`sessionAnchor` / `sessionAnchorEpoch`(**0 = 不在流模式**:从未
82
+ `issueStream()`,或当代 stream 已 `close()`)/ `droppedStaleIngress`(迟到帧丢弃计数)。
83
+ `sessionAnchor` 是**三态**(对抗复审第五轮 [high] 收紧):①有 live stream ⇒ 它开流那一刻的捕获;
84
+ ②无流但账里**有内容** ⇒ 内容归属锚(绝不改报一个还没被用过的现读值 —— 否则「`close()` 之后槽会话
85
+ 轮换」那一刻 `status()` 就报新会话而账里装的还是旧会话的行);③空账无流 ⇒ 兼容入口下一帧会现读的值。
86
+
87
+ 🔴 **单锚不变量**(对抗复审多轮 [high] 采纳):一本 ledger 在**任一时刻只有一个会话锚,而且账里的
88
+ 内容属于那个锚、连接级信任位属于当前那条连接**。五条落地:①stream 模式下 `issueSnapshot()` **继承** stream 的不可变捕获、绝不自己
89
+ 现读;②`issueStream()` **作废**在飞的 snapshot ingress;③**换会话必清内容**:`issueStream()`(以及无 live
90
+ stream 时的 `issueSnapshot()`)发放前先判「账里的内容还算不算数」—— **经 ingress 落的**内容有确切归属锚,
91
+ 按「捕获值变没变」判;**经兼容入口 `ledger.applyFrame()` 落的**内容归属**不可知**(兼容入口每帧现读槽
92
+ 会话,库里不留锚;为它加一次 per-frame 端口读只会给 cli 热路径白白多计一份 miss),不可知就不能替它
93
+ 背书 ⇒ 一律清(实际影响面只有「从兼容入口迁到 ingress」那一次;纯兼容入口宿主从不调 `issue*`,这条
94
+ 判据对它恒不触发)。判为不算数就先清上一会话的行/留存 ——
95
+ ⚠️ **同会话重开流(真正的重连)内容一行不清**,留存池是渲染层宽限窗、重连不该抽走已完成行;判据是
96
+ 「捕获的会话变没变」不是「有没有重开流」。snapshot-only 车道同样走这条(快照的 REPLACE 只换活跃集、
97
+ 刻意不清留存,不清账的话「A 流写完终态留存后 close → 换会话 → 只取快照」会两代同账);
98
+ ④**换连接必清连接级 meta**:`version`/`scoped`/`sessionScoped`/`bgNotifyFailClosed` 是「那条连接」的
99
+ 事实、不是会话内容,而且是 fail-**open** 方向的信任位 —— **每开一条新 stream ingress 就清**,不看会话
100
+ 变没变。重连若落到老版本/降级节点/忽略 `?session=` 的节点,或新 `meta` 缺席/迟到,沿用上一条连接的
101
+ 这两位 = 广播行被投影、foreign 通知绕过本地归属门去改状态并触发钩子。**保留行是对的,保留连接信任位
102
+ 不是**(复位窗内两边都更严:行级回本地台账 = 成文的「宁藏勿串」,通知回 `parentTaskId` 判别;SDK 发帧
103
+ 序 `meta` 第一帧 ⇒ 该窗在真实连接上就是「首帧之前」);⑤`ingress.close()` **真的退出流模式**
104
+ (`sessionAnchorEpoch` 回 0),否则「关流 → 换会话 → 取快照」会继承一条已关的流的陈旧锚 —— 拿旧会话去
105
+ 打服务器而 `status()` 还报旧的;关一只**过期**的 ingress 不影响更新的那一只。
106
+ 没有这几条,「槽内会话轮换 + 宿主没重开流」会造出两个会话代际同写一本 `taskMap` 的混合账,而
107
+ `status().sessionAnchor` 只报一个 ⇒ 混合态从读面上完全看不出来。
108
+
109
+ 🔴 **快照落账的两道闸**(对抗复审第二/四/五轮 [high] 的合流解;任一不满足即丢弃 + 计
110
+ `droppedStaleIngress`):①**发放那一刻有活流的快照一律不落账**(「活流」含两形:`issueStream()` 发过的
111
+ ingress 流,**以及**兼容入口消费、由 `setConnected(true)` 自报的流(⚠️ 后一形**只对纯兼容入口宿主**
112
+ 成立:采用过 ingress 之后流的存活由 epoch 说了算,`close()` 不替宿主改 `connected`,拿一个陈旧的 true 当
113
+ 活流会把「开流 → close → 一次性快照兜底」这条**合法降级序列**的快照永久判废 —— 第九轮 [medium])——
114
+ 兼容车道不设 `liveStreamEpoch`,
115
+ 漏判它就会把一条**正活着**的连接当 snapshot-only 处理、当场清掉它的 meta,而那条流的 `meta` 只在连接
116
+ 首帧出现过 ⇒ 两个信任位在剩余整个连接生命期保持关闭(对抗复审第八轮 [high]);判据固化在**发放**时,不是落账
117
+ 时现查 —— 否则「流活着时签发 → 快照在路上、流先 `close()` → 落账」会因 close 把流模式位清零而**反而通过
118
+ 闸**,而那只快照当初跳过了连接级 meta 复位,会沿用一条**已关连接**的信任位给自己带回来的行背书);②**账本写序号** —— 快照 ingress 发放之后,
119
+ **任何入口**(ingress 或兼容 `ledger.applyFrame`)只要成功落过一帧,这份快照即判过期。第二道是必需的:
120
+ 兼容入口消费活流时 `liveStreamEpoch` **恒为 0**,只看它的闸在那条车道上整条落空(「兼容流写 running →
121
+ 取快照 → 兼容流写 completed → 过期快照 REPLACE 回 running」能整条绕过)。副产品(有意):一只快照
122
+ ingress 是**一次性**的 —— 要再取一次就再 `issueSnapshot()`。
123
+
124
+ 第一道闸的两条独立理由,同一个结论:①快照与流是两条连接、两个写者,而 `applySnapshot`
125
+ 是无条件 REPLACE ——「取样之后、落账之前」流又推了一帧时,照落会把**更新的**行覆盖回旧值,而 fleet 是
126
+ latest-state 总线,没有任何后续帧保证修回来;②快照连接的 `meta` 被 SDK 封装吞掉 ⇒ 它的行**自带零
127
+ scoping 断言**,落进一本正持着别条连接 `sessionScoped=true` 的账里,会被那条不相干连接的信任位无条件
128
+ 放行投影。而 live stream 本身就是「snapshot 先行的 latest-state 总线」,此时再落一份一次性快照零收益;
129
+ 一次性快照的正当场景(首屏 / 温切探针)本来就发生在没有 live stream 的时候。
130
+
131
+ 🔴 **snapshot-only 时快照车道复位连接级 meta**(对抗复审第四轮 [high] 采纳;即使**同会话**;
132
+ ⚠️ 反过来,**活流期间签发的快照什么都不清** —— 内容与 meta 都不动,它自己永久判过期就够了,不该拿一条
133
+ 正活着的连接陪葬,第八轮 [high]):理由同上② —— 快照
134
+ 连接自带不了 `meta`,沿用上一条(已关的)stream 连接的信任位,等于让「快照落在降级 / 不按 `?session=`
135
+ 过滤的节点上」时带回的外来行被一条不相干连接背书,而且此后**没有任何 meta 帧**能纠正它。复位后快照行
136
+ 回到本地台账判别(fail-closed 方向)。⚠️ 推论:只走快照的宿主拿不到 `sessionScoped` 让位 —— 要让位就开 stream。
137
+
138
+ 🔴 **`FleetSnapshotIngress.applySnapshot({tasks, workflows})`**(对抗复审第一轮 [high] 采纳):
139
+ SDK 的 `client.fleet.snapshot()` 回的是 **`{tasks, workflows}`,不是帧**(`fleet.d.ts` 直证:
140
+ `Promise<{tasks: FleetTaskRow[]; workflows: FleetWorkflowRow[]}>`),`applyFrame` 那条口接不上它。
141
+ 本口直吃那个返回值、在库内合成 `snapshot` 帧,端不再手抄一份会漂的帧形;代际闸与 `applyFrame` 同一道。
142
+ ⚠️ 诚实边界:一次性快照拿不到 `meta` 帧 ⇒ 它不填 `sessionScoped` / `bgNotifyFailClosed` 两位。
143
+
144
+ ### ③ 🔴 BEHAVIOR CHANGE(**仅 keyed 车道**):`ownByRoot` 换锚
145
+
146
+ `bg_notification` 的 `ownByRoot` 比对锚从 **`DEFAULT_SESSION_KEY` 槽**换成**本 ledger 的会话锚**
147
+ (经 ingress ⇒ 该 ingress 的不可变捕获;兼容入口 ⇒ `engineSessionParamFor(sessionKey)` 现读)。
148
+ `ownByRoot` 是 own/foreign 丢弃门的**放行腿之一**,换锚**改变放行/丢弃集**:
149
+
150
+ - keyed 宿主**自己的**通知(`rootSessionId` = 本槽会话)修前被判 foreign **整条丢掉**,现在正确放行;
151
+ - 带**别人会话**锚的通知修前被放行(再封顶),现在按方向判 foreign 丢弃、计 `droppedForeignBgNotification`。
152
+
153
+ **默认槽逐字节等价**:未经 ingress + `DEFAULT_SESSION_KEY` ⇒ 锚回落
154
+ `engineSessionParamFor(DEFAULT_SESSION_KEY)` ≡ `engineSessionParam()`,与 0.32.0 同一条取值链。
155
+ **语义副产品(有意)**:`sessionKey` 命名的是**槽**不是会话 uuid —— 槽内会话轮换后,来自该槽**上一会话**的
156
+ 迟到通知会被判 foreign(与 P-13 方向一致的诚实答案)。
157
+
158
+ ### ④ `session_anchor_untrusted` **保词,谓词按臂收窄**(不删词、不换词)
159
+
160
+ 封顶谓词从「非默认 `sessionKey` 一刀切」收窄为「**该臂在本 ledger 上不可自校**」。撤销粒度 =
161
+ **per-arm × per-ledger**,不是 per-version:
162
+
163
+ | 臂 | 是否内容自校 | keyed 上 |
164
+ |---|---|---|
165
+ | `own_root` | ✅ 拿通知的 `rootSessionId` 与本锚**逐值比对**,喂错流会因值不等而拒 | **经 ingress 即解封**;走兼容入口仍封顶 |
166
+ | `server_fail_closed` | ❌ 只读 meta 两个布尔位,不读任何会话、不比对任何值 | 🔴 **恒封顶**,待上游在 fleet `meta` 帧回显本连接 session id 后再议 |
167
+
168
+ ⇒ **`BgNotificationAcceptEvidence` 词表一字未改**(五词照旧),端零改动;变的是**触发条件**。
169
+ 🔴 单会话宿主(默认槽)**结构性不出现**封顶词 —— 这一条与 0.32.0 相同。
170
+
171
+ 🔴 **臂序收紧:只在「成立 ∧ 可信」的会话级臂之间排序**(对抗复审第一轮 [high] 采纳)。旧写法无条件
172
+ 先报 `server_fail_closed`,而**生产 meta 组合两位恒 `true`** ⇒ keyed 车道**永远走不到** `own_root`,
173
+ 哪怕 `rootSessionId` 与本锚精确相等、内容自校已经成立 —— 那等于把一条真的会话级证明扔掉换一个封顶的
174
+ 非证据词,本批的净收益在生产配置下整条不可达。新规矩:第一条**成立且可信**的会话级臂胜出;一条都
175
+ 没有而至少有一条成立 ⇒ 封顶词。🔴 **绝不下探到 `own_parent`**(会话级臂放行的帧没验过
176
+ `parentTaskId ∈ own-run 台账`,报进程成员就是谎报)。默认槽求值序与结果**一字未变**。
177
+
178
+ ### ⑤ P-31 状态:🟡 **部分已解,残余如实留册**
179
+
180
+ **已解**:开流参数与归属判据**同批**换 per-key 锚(只改一半会「按默认槽开流、按 keyed 槽判定」自相矛盾),而且**从写入一直贯到读侧出站** —— owner 台账三值原子写(⑥)+ 五个读点的 wire target / caps 门 / `?session=` 全按行登记的槽取(⑦)。只做前半场的话,「per-key 会话锚」会停在「会话对了、打的服务器与凭证还是别人的」。
181
+ **残余(一族三格,同根因同根治)**:ingress **挡不住**宿主把 A 会话的流喂进 B 的 ingress,而两个
182
+ **连接级零内容自校信任位**只读 meta、不比对任何会话值:①通知臂 `server_fail_closed` ⇒ keyed 上恒封顶;
183
+ ②**行帧车道**的 `meta.sessionScoped` 让位(`ownTaskRow`/`ownWorkflowRow` 命中即整条放行,[1510] 既有
184
+ 裁定)⇒ 被误接的流的行会在本键 ledger 上无条件投影;③**`hook_notice` 分发** —— `onHookNotice(frame,
185
+ sessionScoped)` 的第二参就是那一位,而回调**既不带锚也不带 evidence** ⇒ 被误接的 A 流的「本轮守卫未能
186
+ 评估」会带着「已按会话过滤」这句话进 B 的宿主面,端无从自裁(纯 observe 帧,不改状态)。
187
+ 本批**刻意不封顶行帧车道、也不改 `onHookNotice` 签名**(①都非本批引入;②根治与通知臂同一个;③擅自
188
+ 封顶会让已正确接线的 keyed 宿主的行凭空消失,而 additive 第三参是另一批的公开面决定),现状由常驻钉
189
+ `B3-P31/R4a`(行帧)与 `B3-P31/R9a`(hook_notice)钉住。
190
+ 唯一真根治 = **上游在 fleet `meta` 帧上 additive 回显本连接的 session id**、本端校验相等 ——
191
+ **一次修好三条通道**(按 Wire 能力显式表态制单独立项 **@server @sdk**;本包不阻塞)。
192
+ 🔴 任何把 P-31 写成「已闭合」的表述都是过度声称。
193
+
194
+ **空锚只挡得住 `own_root`**(对抗复审第五轮 [high] 的**带反证驳回半场**):公开面此前写「空锚不解封
195
+ 任何会话级臂」是**过度声称**,已订正。`own_root` 要拿 `rootSessionId` 与本锚逐值比对,没有锚就结构性
196
+ 比不了(`ownByRoot` 恒 false);而 `server_fail_closed` 的前提是「这条流是替谁开的」,**与本端有没有装
197
+ `SessionPort` 无关**(端可能根本没装,而流是宿主自己按别的方式按会话开的)⇒ 默认槽 + 空锚 + 生产 meta
198
+ 仍报 `server_fail_closed`,与 0.32.0 逐字同;给它叠一道 anchorOk 会凭空改默认槽行为。两向由常驻钉
199
+ `B3-P31/R5c` 钉住(正控 + 空锚下 `own_root` 判 foreign 丢弃的负控)。
200
+
201
+ **兼容入口的连接级 meta**(对抗复审第四轮 [high] 的**带反证驳回半场**):兼容入口不调 `issueStream()`,
202
+ 所以走不到「每开新连接必清」那条 —— 但 `case 'meta'` 对四位是**无条件赋值**(不是「键在才写」),而
203
+ SDK 契约里 `meta` 是每条连接的**第一帧**(`fleet.d.ts` 头注逐字:`Emit order: meta → snapshot → …`)
204
+ ⇒ 降级节点 / 老 server 的 meta 一到就把两个信任位清成 `undefined`/`false`。残余窗 = 「同一条连接上、
205
+ meta 之前到达的帧」,按 wire 契约不存在。该无条件赋值由常驻钉 `B3-P31/R4d` 钉住(防「改成键在才写」
206
+ 这类静默回归)。⚠️ 要更强的保护(不依赖对端守序)就用 ingress 车道。
207
+
208
+ ### ⑥ owner 台账**三值原子写**(批 2)
209
+
210
+ 两条 fleet 登记腿(行帧 `case 'task'` 的 parent 映射 / bg 通知的终态腿)从**只写 `runId`** 改为
211
+ `{runId, sessionId, sessionKey}` **同条**写进 `SubagentOwnerRecord`(新增可选位 `sessionKey`,
212
+ `recordBgParentRun` / `recordSubagentOwner` / `recordSubagentOwnerFromProgress` 各加 additive 尾参)。
213
+
214
+ 🔴 **「原子」落在哪**:两个新值取的是**本 ingress 开流那一刻的不可变捕获**(`{session, sessionKey}` 同在
215
+ 一份 `FrameIngressCapture` 里过境),**不是**每帧现读某个可变槽 —— 逐帧现读会把重连后旧流的迟到帧登记成
216
+ 「**旧 runId + 新会话**」,那正是 `recordSubagentOwnerFromProgress` 头注明令禁止的错组合;批 1 的 epoch 闸
217
+ 让旧代际的帧根本进不来,这才是它真正被堵死的地方(不是靠写入点小心)。
218
+
219
+ - **兼容入口 `ledger.applyFrame` 两个新位一个都不写**,与 0.44.0 逐字同:它每帧现读槽会话、库里不留锚
220
+ ⇒ 它写进来的行**归属不可知**,不可知就不能替它背书。keyed 宿主要拿到这两位就用 ingress。
221
+ - **同 `runId` 上缺席不覆盖在场**(`sessionKey` 与 `sessionId` 同一套口径):未迁移的腿不许把 ingress
222
+ 捕获的槽键抹掉 —— 抹掉 ⇒ 读侧退回默认槽的 baseUrl/token/principal,正是批 3 要修的病。
223
+ - **`runId` 真变了 ⇒ 两位一起丢**(那是另一条宿主 run 的会话与槽,留着就是错组合)。
224
+ - 🔴 **同 `runId` 上换会话不再静默**(R6):值仍按「带值的写照常赢」,但会计数并留下四元组
225
+ `{taskId, runId, prior, next}`,读口 `subagentOwnerSessionConflicts()`(**恒应为 0**)。tick 腿与 fleet
226
+ 两腿对同一个宿主 run 给出不同会话,要么是宿主把两条流喂进了同一本账、要么是某一腿的捕获点写错了,
227
+ 两种都是承重缺陷。**判据是计数不是日志** —— 本 store 是零 import 的内核闭包,而且「只落日志的告警在
228
+ 测试里等于不存在」是本仓既有裁定。
229
+
230
+ ### ⑦ 读侧全路径按槽取(批 3;🔴 含**默认槽** BEHAVIOR CHANGE)
231
+
232
+ `resolveOwnerContext(childTaskId)`(新导出)一次读给出 `{record, runId, key}`,
233
+ `key = record.sessionKey ?? DEFAULT_SESSION_KEY`。五个取址点的 **baseUrl / token / principal** 从零参
234
+ `engineWireTarget()`(写死默认槽)换成 `engineWireTargetFor(key)`:
235
+
236
+ | 读点 | 坐标 |
237
+ |---|---|
238
+ | tail | `engineSubagentTail.tailEngineSubagent` |
239
+ | 子代终报 | `engineSubagentOutput.fetchEngineSubagentReport` |
240
+ | taskOutput / taskStop | `engineTaskHandleWire`(caps 门 `engineTaskHandlesCapable(sessionKey)` 同槽) |
241
+ | 委派 prompt 取件 | `engineDelegatedPrompt.readTranscriptWindow` |
242
+ | steer / resume | 会话策略(二态 / 三态)一字未动,只把 wire target 换成 `*For(key)` |
243
+
244
+ `key` 缺席 ⇒ `engineWireTargetFor(DEFAULT_SESSION_KEY)` ≡ `engineWireTarget()`,**单会话宿主逐字节零受迫**。
245
+ 无行上下文的四处(`subagentResumeAvailable` / `auditRetainWithoutWake` / `engineRowStopGate.engineTaskHandlesCapable`
246
+ / `engineCompactWire` 三处)改为 **additive 可选 `sessionKey` 参数**,缺省 `DEFAULT_SESSION_KEY` ——
247
+ 它们答的是「这台引擎有没有这个口 / 给这条会话此刻在飞的 run 发 compact」,owner 台账里没有它们,
248
+ 槽键只能由调用方给;不给一个入口 = keyed 宿主结构性到不了。
249
+
250
+ 🔴 **BEHAVIOR CHANGE(默认槽也变)**:**tail / subagentOutput / taskOutput / taskStop** 四条读面的
251
+ `?session=` 改为**会话二态** —— **行登记时捕到了会话就用行的会话**,缺才退**本槽**现势会话
252
+ (与 steer 腿 0.32.0 起既有的口径统一)。
253
+ 反例今天即可复现、**无需任何 keyed 宿主**:tick 腿(`recordSubagentOwnerFromProgress`)**就在默认槽写
254
+ `sessionId`** ⇒ ①登记 `{runId:R, sessionId:S1}`;②会话轮换到 S2(`/clear` / REPL 内 resume);
255
+ ③**修前**四条读面用 **S2** 打一条 session-bound 的 R = **确定性 404**;④**修后**用 **S1**,与 R 自洽。
256
+
257
+ ⚠️ **taskOutput / taskStop 两腿的会话二态是施工轮扩的**(设计稿 D6 原本只点名 tail/output):病形 =
258
+ 「台账取的 runId 配现势 session」,而这两面**出生即强制带 `?session=``」。批 2 落地之前它们的行没有随行
259
+ 会话、这一格不可达;批 2 让行帧腿开始写 session 之后,它们与 tail/output 变成**同一个形** ——
260
+ 只修 tail/output 就是同一根因只修当格。停行(`stopEngineTask`)尤其不能留:它是有副作用的动作。
261
+
262
+ **plan 决策链两处显式豁免**(`armPlanReviewApproval` / `decidePlanReview`;设计稿 §1 D7b 要求「施工时显式裁
263
+ 入射程或豁免 + 理由」):豁免**不是**因为它没有这个病 —— 它有。理由是**只换这一跳会造出一个新的自相矛盾**:
264
+ 整条 plan_review HITL 链(卡注册 / 退役 / `liveQuestionStore` 的 responder 台账)今天是默认槽单会话形,
265
+ 而 `decidePlanReview` 是从那张卡的 responder 里回调的 ⇒ 半 keyed 形 = 「按默认槽立卡、按 keyed 槽发决断」,
266
+ 比现状更坏(现状至少两头一致地错在同一个槽上,一眼可见)。整条链同批转 keyed 是 additive 公开面改动
267
+ (`armPlanReviewApproval` 要收 `sessionKey` 并捎进 responder 闭包),归后续工单;现状由常驻钉
268
+ `B3-P31/G18b` **两向**钉住(装了 keyed 槽而默认槽为空 ⇒ 仍判 offline / 默认槽装上照常 arm)。
269
+ 🔴 **豁免不等于没有后果,后果如实登记**(异源对抗复审 [high] 采纳的半场,同步进 `INTEGRATION-CLIENTS` §7c 的 P-31 行):
270
+ ①**keyed-only 宿主**(默认槽为空)上 `armPlanReviewApproval()` 恒返 `false` ⇒ 一条 park 在 `plan_review` 的 run
271
+ **没有审批入口**;②**keyed + 默认槽都装**的宿主上,卡与决断都走**默认槽**的 baseUrl/token/principal —— 与那条
272
+ park 住的 run 所属的槽可能不是同一台。⇒ **多会话宿主在 plan-mode 上今天不可用**,不是「有一点瑕疵」。
273
+
274
+ 🔴 **`/compact` 一条动词的槽键必须一以贯之**(异源对抗复审 [high] 采纳,本批**已修**):首版只把
275
+ target 与 `?session=` 换成 keyed,而 **run id 仍读默认槽**、**arm 状态是进程级布尔**、**能力位是进程级单值**
276
+ —— 那正是本批用来给 plan 决策链判豁免的那个「半 keyed 比现状更坏」的形,自己却踩了。两条可达的错:
277
+ ①`requestEngineCompact('B')` 会把 **A 的 run id** 发到 B 的服务器(动了一条不相干会话的上下文);
278
+ ②B 的 idle-arm 被**先 bind 的那条会话**消费掉 —— A 的 run 被压缩,而 B 自己永远没压缩。
279
+ 修法 = 五件全按同一个 key:新增 `activeEngineRunIdFor(sessionKey)`(零参 `activeEngineRunId()` 收敛成它的
280
+ 默认槽兼容层,取值链逐字等价);`pendingEngineCompact` 布尔 → `pendingCompactKeys: Set<string>`
281
+ (`isEngineCompactPending(sessionKey?)` 缺省默认槽);`manualCompactCapable`/`capabilitiesProbe` 两个进程级
282
+ 单值 → **per-baseUrl 分桶**(与 `engineCapsCache` / `engineSubagentOutput.capByBase` 同代);
283
+ `resetManualCompactCapability()` 连 arm 台账一并清。⚠️ 单会话宿主只有一个 baseUrl / 一个槽 ⇒ 逐字节零变化。
284
+
285
+ 🔴 **异源对抗复审第二轮的三条跟修**(2×high 采纳半场 + 1×medium 全采):
286
+ ①**两个槽同时认领同一条 run ⇒ 出身对整对作废**(`runId` 不丢)。复审原议是「任何出身冲突都
287
+ fail-closed」,**只采窄的那一格**:只换会话、槽键不变那一格是 **#242 批 2 已裁并钉死**的
288
+ (常驻钉 `#242-R3c3`「带 session 的写照常覆盖,新值赢」),施工轮无权把别的批次的裁定翻面;
289
+ 而「两边都带槽键且不相等」是本批**新引入**、没有任何既有裁定、且不可能有合法读法的一格 ——
290
+ 两台引擎、两份凭证同时声称同一条宿主 run 是谁的,而读侧拿它挑的正是服务器/凭证/会话。
291
+ 留痕在库内**零消费者**,拿后到的那份继续路由就是把不可知说成已知 ⇒ 按「错值比缺席更坏」整对丢,
292
+ 读侧回落到「无通道证据」那条**成文的**降级路径。⚠️ 冲突计数恒应为 0 ⇒ 对接线正确的宿主不可达。
293
+ ②**`/compact` 的延迟发射冻结同一代际**:`onEngineTaskBound` 的 1.5s 延迟发射与 404 的 2s 重试此前只
294
+ 带 `{taskId, sessionKey}`,target 与 `?session=` **到发射那一刻才现读** ⇒ 期间 `/clear`/槽重绑/引擎替换,
295
+ 旧 taskId 就配上了新引擎与新会话(「五件共享同一个字符串 key」≠「五件来自同一代际」)。改成在
296
+ request/bind 那一拍冻一只不可变 `CompactShot{taskId, baseUrl, token?, principal?, session?}`,延迟与重试
297
+ 一律用它。⚠️ 快照**不含**能力位:那一位按 baseUrl 缓存、只决定「发不发」,冻进来会让一次真降级发不出信号。
298
+ ③**`resolveOwnerRunId` 的在飞 run 改惰性求值**:此前是**默认参**,每次调用都先读一遍**默认槽**的
299
+ `SessionPort` —— 哪怕台账里有这一行、第三态根本走不到。批 3 之后 keyed 宿主是成文支持的路径,那次白读
300
+ 会给 keyed-only 宿主刷 `hostPortMisses()`,而接入档要求宿主自检**恒空**:一个恒响的假警报会把真漏装淹掉。
301
+ 注入语义一字未变。
302
+
303
+ 🔴 **异源对抗复审第三轮的三条跟修**(1×high 全采 + 1×high 一半采纳一半带反证驳回 + 1×medium 全采):
304
+ ①**冲突判据按「有效槽」比,不按「键在不在」比**:读侧 `resolveOwnerContext` 把**缺席的 `sessionKey`
305
+ 读成 `DEFAULT_SESSION_KEY`** ⇒「不带键的带出身写」不是「没有槽」,它就是**默认槽**在认领这条 run。
306
+ 只比显式值会漏掉最常见的一格 —— keyed ingress 写 `{S1,'A'}` 之后,一个仍按**三参老签名**调用的 tick
307
+ (`{S2, 无键}`)会把记录改成 `{sessionId:S2}` ⇒ 读侧拿 **A 的 runId** 去打**默认槽**的服务器与凭证。
308
+ 两向都挡。⚠️ 单会话宿主上两边的有效槽恒是 `DEFAULT_SESSION_KEY` ⇒ 本分支恒不触发,`#242-R3c3` 一字未动。
309
+ ②**`/compact` 的 arm:冻不出快照就不消费**(采纳半场)—— 那一拍没有 wire 时,旧形先把 arm 划掉再发现
310
+ 发不出去 = 用户的 `/compact` 无声蒸发,而命令行早就回过 `'armed'`。改成**冻出来了才划掉**,这一拍发不出
311
+ 就留着、下一次 bind 再兑现。**📋 带反证的驳回半场**:复审还要求把快照提前到 **arm 那一拍** —— 不采,
312
+ 反证是 arm 那一拍**根本没有 taskId**:`/compact` 在 idle 下的成文语义是「**这个槽的下一条 run**」,
313
+ 槽的身份就是 key;槽真被重绑到另一台引擎时「下一条 run」就在那台上,压它才是意图,而把 arm 那一刻的
314
+ target 冻下来反而会去压一台**已经不再跑这条会话**的引擎。⇒ 冻结的正确起点是**拿到 taskId 的那一拍**。
315
+ 现状(含驳回半场)由 `B3-P31/R6i` 两半各自钉住。
316
+ ③**`text_end` 的 lane 身份位坏了就整帧 fail-closed**:`parentToolCallId` 是**子流断闸的锚**,按
317
+ 「不是串就当没有」处理会把一条**子代**的段边界擦掉 lane 标记、当成 **leader** 的段边界送到宿主面
318
+ (fail-**open** 方向的跨 lane 状态破坏)。三态与本包 `bg_notification` 的 `parentTaskId`(`B3-DIRTY`)
319
+ 逐字同族:缺席 ⇒ 本来就是 leader 帧;键在场却非串(含 `null`)⇒ `malformed` 丢弃并留痕。
320
+ chrome 层的**第二道 carrier 判**同批收紧成按「键在不在」判(给非投影口喂进来的帧兜底)——
321
+ 同族其它臂的 `typeof === 'string'` 写法在它们那里只影响一行装饰,在本臂上决定的是「这条边界算谁的」。
322
+
323
+ 🔴 **异源对抗复审第四轮**(2×medium 采纳 + 1×high **带反证驳回**):
324
+ ①**能力重置不动 arm 台账**(修本批自己引入的默认槽回归):`resetManualCompactCapability()` 是**生产口**
325
+ (引擎重启 / respawn 的能力失效),不是「取消用户的 `/compact`」。第二轮顺手加的 `pendingCompactKeys.clear()`
326
+ 会让「idle 时 `/compact` 回过 `armed` → 引擎 respawn → 下一条 run 不再压缩」——命令无声蒸发,而且那是
327
+ **默认槽**上的行为改变,与本批「单会话宿主逐字节零变化」的声明直接矛盾。清 arm 改走新测试钩
328
+ `__resetEngineCompactArmForTests()`(生产面没有这个动作)。
329
+ ②**定时器也按本槽取,而且排上队才划掉 arm**:`deferFire` 读的是零参 `hostTimers()`(= 默认槽)——
330
+ 按接入契约只走 `installHostFor(key, {timers})` 的 **keyed-only 宿主**在那里恒空 ⇒ 快照有效、arm 已划掉、
331
+ 请求永远不发射。改成 `hostTimersFor(sessionKey)`(默认槽下逐字节等价),并把 `deferFire` 改成返回是否
332
+ 排上了队 —— **排上了才划掉 arm**(冻得出快照但排不上队时先划掉,同样是把命令无声吃掉)。
333
+ ⇒ 一条动词的槽键一以贯之从五件变成**六件**:target / `?session=` / run id / 能力位 / arm 台账 / **定时器**。
334
+ 🔴 **第五轮 [high] 采纳,推翻本批前一版的「留 runId、只清出身」**:前一版的理由是「`runId` 那一位
335
+ 没有争议」——**那条理由不成立**。`runId` 要能用,读侧必须给它配一个槽,而读侧对缺席 `sessionKey` 的
336
+ 解释是 `DEFAULT_SESSION_KEY` ⇒「留 runId、清出身」实际等于**断言这条 run 属于默认槽**,而冲突恰恰是
337
+ 「有一个 keyed 槽也认领了它」的**正面证据**:那不是「不知道」,是一个已经被证否的猜测。它与兼容入口的
338
+ 无槽记录**不同命** —— 对纯兼容宿主默认槽就是它唯一的槽(记录是对的),在冲突这一格上默认槽是被证否的
339
+ 候选之一。⇒ **整条记录删掉**。
340
+ 🔴 **删记录不需要任何读侧改动**:五个读点对「台账缺席」早已是成文的诚实缺席(`noteBgOwnerAbsence`
341
+ 留痕 + null / no-run,#242 批 3 [4000] Q3=B 裁定),本分支直接复用它。
342
+ 🔴 **隔离是粘的**(第六轮 [high] 采纳,推翻本批前一版的「后一帧自愈」):删完之后 `prior` 为空 ⇒
343
+ **任一条**后续帧都会把记录重建出来 —— 序列 `A 写 → B 写(冲突删)→ A 再写` 当场恢复 `{sessionKey:'A'}`,
344
+ 两个来源**持续产帧**时这条行会在两个槽之间来回翻,每翻一次开一个错误出站窗,而读侧拿它做的是
345
+ stop / resume 这类**有副作用**的动作。「后一帧自愈」只是**假设**另一条流已经停了 —— 那不是证据。
346
+ ⇒ 新增隔离台账 `conflictQuarantine`(`taskId` → 出事的那个 `runId`,有界、与 owner 表同 LRU 口径):
347
+ 隔离期间记录**保持缺席**,任何后续帧都不许重建;**解除规则 = `runId` 变了**(那是另一条宿主 run,
348
+ 与出事的那一条无关)—— 不是把这个 taskId 永久钉死。隔离期间零读侧改动:五个读点走成文的诚实缺席。
349
+ 出站反钉 `B3-P31/R6k`:默认槽 + 两个 keyed 槽**同时装配**、两个来源**持续交替**再写几帧,
350
+ 之后三台服务器一次都没被碰(并带反假绿前提:先把三台 caps 门喂成 true)。
351
+ 🔴 **隔离是「墓碑」不是可解除的锁**(第七轮 [high] 采纳,推翻本批前一版的「`runId` 变了就删条目」):
352
+ `runId` 是**无序字符串**,「不相等」证明不了前进方向。可复现序列:`(C,R)` 冲突 ⇒ 隔离;`(C,R2)` 把条目
353
+ **删掉**并登记 R2;此时原冲突源**迟到**的 `(C,R)` 因条目已删、`sameRun` 又为假,直接把映射覆盖回旧 run
354
+ 与旧槽 —— 误路由窗当场重开。⇒ 条目**留着不删**:别的 `runId` 照常放行(另一条宿主 run 与出事的那条
355
+ 无关),而**那一条**出事的 run 从此不许再回来(`B3-P31/R6n` 两向钉住)。
356
+
357
+ 🔴 **plan-review 的豁免从「文档登记」升成「运行时边界」**(第六轮 [high] 采纳):
358
+ `armPlanReviewApproval(result, sessionKey?)` 加 additive 第二参 —— 传**非默认槽** ⇒ **运行时 fail-closed**
359
+ (不 arm + `hostLog('error')` 说明理由),而不是默默按默认槽立卡、再按默认槽把决断发出去(那会把
360
+ B 会话的 approve/reject 带着**默认槽**的 baseUrl/token/principal 发出去:对端有同名任务就改错会话的
361
+ 状态,没有就 404 而原 run 继续停在审批门)。不传 / 传默认槽 ⇒ 与 0.44.0 逐字同。整条链转 keyed 仍归
362
+ 后续工单 —— 但在那之前**明确拒绝**比静默回落诚实,「文档登记不能代替运行时隔离」。
363
+ 🔴 **重开入口与 arm 入口同命**(第七轮 [high] 采纳):`reopenPlanReviewCard(taskId, {sessionKey:'B'})`
364
+ 按 B 槽发卡与记账,但**缺省 responder 的投递走 `decidePlanReview`,而那一处固定打默认槽** ⇒ 用户在 B
365
+ 看到并点下去的 approve/reject 带着 **A 的** baseUrl/token/principal 出站。arm 臂那道闸覆盖不到这条
366
+ 409/自愈重开入口 ⇒ 同款 fail-closed:非默认槽**且没有自带 `deliverDecision`** 时拒绝重开。
367
+ ⚠️ 宿主**自带 `deliverDecision`**(它自己按槽投递)⇒ 放行 —— 本闸拦的是「库内缺省投递口打错槽」,
368
+ 不是把 keyed 重开整条关掉。
369
+ 📋 **带反证的驳回半场**:复审还要求把 `sessionKey` 做成**必填**、并让 `plan_review_park` 事件带上它。
370
+ **不采**:(a) 把已发布 API 的可选参改必填是**破坏性**改动,本批的公开面纪律是全 additive;
371
+ (b) 让 chrome 事件带槽键要动 `runStream` 的发射面 —— 那是 plan-review 整条链转 keyed 那一批的活,
372
+ 本批把它当尾巴捎上只会造出又一个半 keyed 形。⇒ 如实登记为**后续工单的第一条**,并在接入档的装配
373
+ 自检清单上写明「多会话宿主 plan-mode 今天不可用」。
374
+
375
+ **默认槽等价性的精确边界**(如实记,不写成「零变化」):除上面点名的 `?session=` 之外,四条读面的两道闸
376
+ **次序一字未动**(无 wire 仍先于「台账缺席」返回,缺席留痕不会在离线宿主上刷屏);唯一的求值序差分是
377
+ owner 台账的一次 **LRU touch 提前到了 wire 闸之前**(槽键要先拿到才能取 wire target)—— 影响面止于
378
+ 「离线宿主上 320 条满表时的淘汰顺序」,不改任何判决。
379
+
380
+ ### ⑧ `text_end` 段边界投影臂(#323 / core #447)
381
+
382
+ 引擎新事件 `{type:'text_end', content}`(core ≥5.63 `dist/core/types.d.ts` 的 `TaskEvent` 臂
383
+ `& TaskEventIdentity`;server ≥7.50 **三腿都在发** —— live SSE 与 durable 两处投影在
384
+ `http/routes/tasks.js`,subagent tail 在 `fleet/subagent-tail-bus.js`)。
385
+ 语义 = 「模型关掉了那个 text content block」,`content` = **该段的权威全文**(与那一段 `text_delta` 的
386
+ 拼接逐字节相等,取自 brain 自己的累加)。CC 对位:CC 在 provider 的 `content_block_stop` 上把每个写完的
387
+ 块当作一条独立 assistant 消息 —— 块结束**就是**分段信号,本臂是同一个边界被搬到 TaskEvent wire 上。
388
+
389
+ - 投影层:`eventToSdkMessage` 出**中性内部臂** `text_end`(登记进 `INTERNAL_SDK_ARM_TYPES`),
390
+ `content` + `eventId` + `parentToolCallId` 原样透传;
391
+ - chrome 层:`adapt` 出新臂 **`ChromeEvent{kind:'text_segment_end', content, eventId?}`**
392
+ (`CHROME_ARMS` 已登记,宿主义务写在 `seam.ts` 的臂注上)。**子流断闸** —— 带 `parentToolCallId` 的
393
+ 段边界属于子代/编排流,不上 leader 面(拿它去提交宿主的段 = 跨 lane 状态破坏);要消费子流边界的
394
+ 宿主读 SDKMessage 平面那一层。
395
+ - 🔴 **绝不铸 transcript 行**:`content` 的字节**已经**以活体增量流过、committed 形另有出口,再渲一行
396
+ 就是同一段文字**上屏两遍**。它的正当用途是**对账**(用引擎的权威全文校自己缝合的那一段)与**定界**。
397
+ - 🔴 **绝不 `not_in_slice` 静默丢**:那一档走 `kind:'none'`,是静默的;本帧带着消费方真正需要的判别
398
+ 信号,静默丢 = 把一个真实能力缺口做成 fail-open(本文件对 `approval_request` 那段头注点名的病形)。
399
+ - **空段对位 core 的「空段无帧」**:`content` 空串 ⇒ `none/empty_payload`(不是 `malformed` —— 帧不畸形;
400
+ 也不是 message —— 没有段可提交,一个没有段的边界会渲出幻影行);`content` 非串 ⇒ `dropped/malformed`。
401
+ - 🔴 **宿主消费纪律:诚实缺席,不可反推**(core 臂注逐字):只有**会报块结束的 Brain** 才发它(三个一方
402
+ brain 都发,自定义 brain 可能整条流一帧都没有)⇒ 缺席 = 「**没报**」,**永远不等于**「这一段没结束」。
403
+ 要退回自家启发式必须按**整条流**判(这条流一帧都没带),按**单帧**缺席就回落 = 把「这一拍还没到边界」
404
+ 误当「引擎不支持」。
405
+
406
+ **SDK 账(如实记,没有可提的货)**:`@sema-agent/sdk` 的 **latest 就是 `7.2.0`**(本批当日
407
+ `npm view @sema-agent/sdk dist-tags` 直读),它的 `AgentEvent` union **没有** `text_end`(真 tarball
408
+ `dist/` 全树零命中)。⇒ **devDep 不动、peer floor `>=7.2.0` 不动**(抬无可抬;而且本臂**不硬依赖**新
409
+ union 成员 —— 它按 raw 读,旧 SDK 下编译与运行都成立,抬 floor 只会凭空排除合法宿主)。
410
+ 臂因此走 **raw 预分派**(与 `engine_notice` 同姿势),**自退休不靠人记**:预分派用
411
+ `(ev as {type?:unknown}).type` 形、**不收窄** `ev` ⇒ 臂一进 union,`switch` 的 `default` 仍看得见它,
412
+ B5 穷举断言 `assertNeverArm` **编译期真红**,逼下一棒把它搬进 `switch`。
413
+
414
+ 📋 **本批如实留白(不在本批做)**:**不撤** `adapt/textStream.ts` 的 idle-flush 启发式
415
+ (`takeAnswerSegmentOnIdle`,#323 症状① 的止血件)。撤它要先按上面那条「诚实缺席」纪律做一条
416
+ **per-stream 的状态化策略**(整条流一帧都没有才回落),那是行为面改动,按宪法三问单独走。
417
+ 本批只把边界**送到宿主手上**(壳侧接线是下一棒)。常驻钉 `#323-k` 两向钉住「本批没顺手改分段行为」:
418
+ 同一 turn 里活体增量 + 段边界 + 收口,转录仍只提交**一条** assistant 文本消息,而 chrome 信号确实发了
419
+ —— 下一批真要改时这条会红,逼那一批**显式**改判据。
420
+
421
+ ### ⑨ 消费方波及(表态制登记素材)
422
+
423
+ | 端 | 波及 | 强制否 |
424
+ |---|---|---|
425
+ | **cli** | ①fleet 台账唯一真消费方,走默认槽 ⇒ 批 2/3 的库内修复**提货即得、零改码**;⚠️ 但 ⑦ 的 `?session=` 二态是**默认槽行为改变**,会话轮换后读旧行的出站参数今昔不同(方向是修复:此前确定性 404)。②`text_end` 壳侧接线(消费 `text_segment_end` chrome 臂 / 撤 idle-flush)**候** —— 本包只发信号 | 否(①零改码;②候接线) |
426
+ | **web** | 走 `createBackgroundView` 的会话外读面,不用 `createFleetLedger`;`text_end` 按现状(未接 chrome 臂 = 该面不启用,不是错误态) | 否 |
427
+ | **desktop** | fleet 消费为零(C-R11:线冻结待启,重启时按消费矩阵一次性补消化) | 否 |
428
+
429
+ ### ⑩ 门
430
+
431
+ `scripts/run-client-core-pure-test.mjs` **B3-P31 段 +22 条**(G1 开流参数分槽 5 / G2 keyed `own_root` 解封 1 /
432
+ G3 跨槽负控 1 / G5b `server_fail_closed` keyed 恒封顶 2 / G6 空锚不解封 2 / G7 epoch 迟到帧 4 /
433
+ G8 snapshot 独立 ingress 3 / G9+G10 默认槽回归锚 3 / G4b keyed 兼容入口仍封顶 1),
434
+ `MIN_R242B3_CHECKS` 100→**167**(零松量;对抗复审四轮 9×high+1×medium 采纳、1 条带反证登记、
435
+ 变异自证补钉 2 条、第五轮 +6、第六轮 +4、第七轮 +2、第八轮 +2、第九轮 +2、第十轮 +2 后共 +45:R10a 活流分支绝不现读端口(正控 + snapshot-only 照常现读的
436
+ 反钉)2 /R9a `hook_notice` = P-31 残余第三格(钉现状)1 /
437
+ R9b `connected` 只对纯兼容入口宿主有权威性 1 /R8a 活流(兼容入口 + `setConnected` 自报)上签发
438
+ 快照不许清那条流的 meta(正控 + 未自报连接仍按 snapshot-only 的反钉)2 /R7a 快照闸 close-before-result 竞态(正控 + 「发放时无流照常
439
+ 落账」反钉)2 /R6a 兼容入口存量迁 ingress 时一律清 3(含纯兼容宿主
440
+ 零受迫反钉)/ R6b `close` 退出流模式 ≠ 锚回落现读 1 /R5a `close` 后 status 锚报「内容属于谁」2 /
441
+ R5b 账本写序号全入口通用 1 / R5c 空锚只挡得住 `own_root`(正控 + 负控)3 /G5c 生产 meta 下 `own_root` 必须可达 1 / G7b ingress 锚是开流那一刻的不可变
442
+ 捕获(正反两向)2 / G8b 单锚不变量与 `applySnapshot` 5 / R2a 换会话≠重连的清账与反钉 3 /
443
+ R2b 有 live stream 时快照不落账 1 / R2c `close` 真退出流模式 3 / R3a snapshot-only 换会话清账 4(含
444
+ 「把让位重新打开后旧行仍不在」的成因判别钉)/ R3b 连接级 meta 每开新连接必清 2 /
445
+ R4c 同会话流活着时快照同样不落账 1 / R4b 快照车道一律复位连接级 meta 2 /
446
+ R4a 行帧车道让位=在册残余(钉现状)1 / R4d `meta` 帧对四位无条件赋值 2)。
447
+
448
+ **批 2+3 跟钉:B3-P31 段再 +20 条,`MIN_R242B3_CHECKS` 167→187**(零松量):G13 行帧腿三值原子写 1 /
449
+ G14 通知腿三值原子写 1 + 放行前提 1 / G15 兼容入口回归(只有 `runId`)1 / G16 缺席不覆盖在场(`sessionKey`
450
+ 半场)1 / G17 `runId` 变更两位一起丢 1 / R6a 同 `runId` 换会话不静默 4(前提 + 同会话不计数反钉 + 四元组 +
451
+ 留痕不改判决)/ **G18 读侧配对 8**(设计稿标注**不可免验**;构造 = 两台 stub server,keyed 槽一台、默认槽
452
+ 一台,断言请求落在哪一台 + 那台上的 query 与 `authorization`/`x-agent-principal` 两个头):终报前提 1 +
453
+ 落对槽 1 + 凭证按槽 1 + `?session=` 取**行登记**会话而非现势 1 + taskOutput 腿同款 1 + **tail 腿同款 1** +
454
+ 默认槽零受迫负控 1 + 台账缺席零出站反钉 1 / G18b plan 决策链豁免钉现状 2。
455
+ **新段 `#323`(`MIN_TEXTEND_CHECKS` 首建 19,零松量)**:投影口 11(内部臂词表登记 1 + 投影三面 3 +
456
+ 身份两键正反 2 + `content` 四坏形 4 + 空段 `empty_payload` 1)+ chrome 面 6(义务表登记 / 事件到端 /
457
+ 不铸转录 / `eventId` / 子流断闸 / 第二道 carrier 判)+ **本批不动分段行为的正反 2**。
458
+
459
+ **批 2+3 与 #323 的红先绿后两证**:产品源整树退回批 0+1 的 `HEAD`(逐文件 `git show HEAD:<path>`,
460
+ `src/` 先 `cp -R` 备份)⇒ 首个新钉 `B3-P31/G13` 当场 AssertionError;同状态下 `text_end` 探针直读
461
+ `eventToSdkMessage` 返回 `{kind:'dropped', why:'unknown_arm', type:'text_end'}`、
462
+ `INTERNAL_SDK_ARM_TYPES.has('text_end') === false`、`CHROME_ARMS` 无 `text_segment_end` ——
463
+ **修前它是被丢弃的未知臂,不是「本来就没渲染物」**。随后 `cp -R` 复原(`diff -r --brief` 逐文件核对
464
+ byte-identical)⇒ 全段绿。
465
+
466
+ **异源对抗复审跟钉:B3-P31 段再 +14 条,`MIN_R242B3_CHECKS` 187→201**(零松量;3×high 全采纳):
467
+ R6a 换**槽键**同样计冲突 1(原写法只锚 `sessionId` ⇒ 「只换槽键」那一格恒静默)/ R6b **出身对整对进出** 4
468
+ (带槽键无会话的 ingress 写整对赢 + 只带会话的 tick 写整对赢 + 两次换出身都计冲突 + 两位皆缺席不改出身的反钉)/
469
+ R6c `/compact` 槽键一以贯之 6 / R6d `activeEngineRunIdFor` per-key 与零参等价 3。
470
+
471
+ **批 2+3 与 #323 的变异自证 8 发**(逐发 cp 备份 → 打点 → 跑套 → cp 复原 + `cmp` 逐字节核对;
472
+ **不编译的形不计数**):①`resolveOwnerContext` 的 `key` 恒默认槽(批 3 全族漏换)→ G18 抓;②行帧腿只传
473
+ `session` 不传 `sessionKey`(批 2 半写)→ G13 抓;③`text_end` 预分派整条摘除 → #323-b 抓;④`text_end` 的
474
+ chrome 臂从 `ARMS` 表摘除(内部臂到端那一跳断掉)→ #323-g 抓;⑤`sessionKey` 的「缺席不覆盖在场」撤掉 →
475
+ G16 抓;⑥taskHandle 会话二态摘除 → G18 taskOutput 腿抓;⑦output 腿会话二态**优先序反转** → G18 `?session=`
476
+ 那条抓(⚠️ 首次用的「直接删掉 `owner?.sessionId ??`」形**不编译**(`noUnusedLocals`),按纪律不计数,
477
+ 改成可编译的反转形重打);⑧**tail 腿**会话二态优先序反转 → **首轮存活**(G18 当时只覆盖 output 与
478
+ taskOutput 两腿,而三条读面是同形存量)⇒ 补 tail 腿钉(floor 186→187),复跑抓红。
479
+
480
+ **第二轮跟钉:B3-P31 段再 +12 条,`MIN_R242B3_CHECKS` 201→213**(零松量):R6e 跨槽争同一条 run 的
481
+ 整对作废 5(含「同槽换会话**不**作废」的窄化反钉 —— 判别力本体就在这一条上)/ R6f `/compact` 延迟发射
482
+ 冻结同一代际 4(stub server 两台 + TimersPort:bind 之后、发射之前把槽整体换掉,判据锚在「打的是哪一台 +
483
+ 带哪个 `?session=`」)/ R6g 在飞 run 惰性求值 3。
484
+
485
+ **第三轮跟钉:B3-P31 段 213→221(+8)、`#323` 段 19→29(+10)**(均零松量):R6h 有效槽判据 4
486
+ (含「显式默认槽 vs 隐式默认槽 = 同一个槽」的窄化反钉 —— 判别力本体)/ R6i `/compact` arm 两半 4 /
487
+ `#323-d2` lane 位坏形 fail-closed 10(投影层五坏形 + 真缺席仍 leader 的反钉 + chrome 半场三坏形 +
488
+ 真缺席照常出 leader 的反钉)。
489
+
490
+ **第四轮跟钉:B3-P31 段 221→226(+5)**(零松量):R6j arm 的两条「无声吃掉」路径 4(能力重置不动
491
+ arm 台账 / `deferFire` 排不上队不划掉 arm / 装上本槽 TimersPort 后同一记 arm 被兑现的反钉 / 前提 armed)
492
+ + R6c 定时器按本槽取 1。测试钩 `__resetEngineCompactArmForTests` 入基线(`testHookCount` 40→41)。
493
+
494
+ **第五轮跟钉:B3-P31 段 226→232(+6)**(零松量):R6e/R6h 改断言「整条记录删掉」+ 取址口诚实缺席 +
495
+ 恢复规则(唯一槽证据自愈)2 / **R6k 出站反钉 4** —— 判据锚在「那条有争议的 run 有没有被发到某台服务器
496
+ 上」这个**真正决定伤害的量**上,并带一条**反假绿前提**(先把三台的 caps 门喂成 true:`fetchEngineTaskOutput`
497
+ 的 caps 门排在「台账缺席」之前,不喂就会红在 caps 未判上,那证明不了本条)。
498
+
499
+ **第六轮跟钉:B3-P31 段 232→236(+4)**(零松量):R6l 隔离是粘的 3(A 再写一帧也不许重建 / 两个来源
500
+ 持续交替写逐次核仍缺席 / `runId` 变了即解除的反钉)+ R6m plan-review 非默认槽运行时 fail-closed 2
501
+ (显式非默认槽拒 arm + 显式默认槽照常 arm 的反钉);R6k 同批补「两个来源持续交替写」那一段。
502
+ `singleton-manifest` 登记 `conflictQuarantine`(262 条),`HIGH_RISK_CEILING` 93→94 —— 显式抬 1 并写明
503
+ **为什么它必须是进程级单例**:它是 `bgParentRun` 的配对物,隔离闸与被隔离的那本账必须同实例;
504
+ 改 per-session 是**反向**的(它记的恰恰是「这条行被两个 session 槽同时认领了」)。
505
+
506
+ **第七轮跟钉:B3-P31 段 236→241(+5)**(零松量):R6n 墓碑两向 2 / R6o 重开入口 3(含**反假绿前提**:
507
+ 先挂 PR-KEY 槽的 overlay —— 不挂的话两条断言都会因「没有 overlay」回 false,本闸的判别力为零)。
508
+
509
+ **第七轮变异自证 2 发**:㉗隔离条目退回「别的 runId 就删掉」→ R6n 抓;㉘reopen 非默认槽闸摘除 → R6o 抓。
510
+
511
+ **第六轮变异自证 3 发**:㉔隔离闸摘除(冲突后下一帧照样重建)→ R6l 抓;㉕隔离不按 `runId` 解除
512
+ (永久钉死)→ R6l 解除反钉抓;㉖plan-review 非默认槽静默回落默认槽 → R6m 抓。
513
+
514
+ **第五轮变异自证 1 发**:㉓冲突后退回「留 runId 清出身」→ R6k 前提抓。
515
+
516
+ **第四轮变异自证 3 发**:⑳能力重置又去清 arm 台账 → R6j 抓;㉑`deferFire` 退回零参 `hostTimers()`
517
+ → R6c 抓;㉒排不上队仍划掉 arm → R6j 抓。
518
+
519
+ **第三轮变异自证 4 发**:⑯冲突判据退回「两边都显式才比」→ R6h 抓;⑰arm 退回「先划掉再冻结」→ R6i 抓;
520
+ ⑱投影层 lane 位坏形不 fail-closed → `#323-d2` 抓;⑲chrome 断闸退回 `typeof === 'string'` → `#323-d2`
521
+ chrome 半场抓。(⑯前两次形要么不编译(`noUnusedLocals`)、要么红在更早的 `#242-R3c3` 上 —— 那证明不了
522
+ 「隐式默认槽这一格漏了」,故改用第二轮那一版的精确形重打。)
523
+
524
+ **第二轮变异自证 3 发**:⑬跨槽冲突的作废分支摘除 → R6e 抓;⑭`/compact` 延迟发射退回「发射那一刻现读」
525
+ → R6f 抓;⑮`resolveOwnerRunId` 退回默认参急切求值 → R6g 抓。(⑬首次用的「把 `!==` 换成 `false`」形
526
+ **不编译**(`exactOptionalPropertyTypes` 下窄化随之失效),按纪律不计数,改「整个分支摘除」的可编译形重打。)
527
+
528
+ **第一轮跟钉后再 4 发**(同款 cp+cmp 纪律):⑨出身对退回**逐位独立 merge**(异源对抗复审 [high]①的精确修前形)→ R6b 抓;
529
+ ⑩`/compact` run id 退回默认槽 → R6c 抓;⑪`/compact` arm 退回进程级(任一 bind 都消费)→ R6c 抓;
530
+ ⑫冲突判据退回只锚 `sessionId` → R6a 抓。(⑨首次用的「`carriesProvenance = false`」形太钝 —— 它把**所有**
531
+ 带出身的写一起关掉,红在更早的 `#242-R1b`,证明不了「分开 merge 会合成混合元组」这一条,故改精确形重打。)
532
+
533
+ **变异自证 12 发**(逐发 cp 备份 → 打点 → 跑套 → cp 复原 + `cmp` 逐字节核对;不编译的形不计数):
534
+ ①`ownByRoot` 锚退回默认槽 → G4 抓;②keyed 兼容入口的 `own_root` 当可信 → G4b 抓;③换会话不清账 →
535
+ R2a 抓;④快照在 live stream 时照落 → R2b 抓;⑤连接级 meta 只在换会话时清 → R3b 抓;⑥**ingress 锚改成
536
+ 逐帧现读 → 首轮存活**(既有钉全靠 epoch 闸挡旧 ingress,从没验过**当代** ingress 的锚会不会跟着槽漂)
537
+ ⇒ 补 G7b 正反两向,复跑抓红;⑦**snapshot-only 换会话不清账 → 首轮存活**(第四轮的 meta 复位一落地就把
538
+ R3a 原断言的判别力吃掉了:「行不见了」有「真清账」与「被 own 判别藏起来」两个成因)⇒ 补一条「把让位
539
+ 重新打开后旧行仍不在」,复跑抓红;⑧`close` 不退出流模式 → R3a 抓;⑨臂序退回「第一条成立的臂胜出」→
540
+ G5c 抓;⑩快照写序号闸放宽成 `>=`(等于撤掉)→ R5b 抓;⑪`status` 锚在无流时直接现读(不看内容归属)→
541
+ R5a 抓;⑫兼容入口存量不算「归属不可知」→ R6a 抓。原两条 P-31 现状钉按设计处置:钉①**翻面**成「判 foreign 丢弃」
542
+ (那正是换锚后的正确方向),钉②**语义原样保留**;同段全部 `seen[n]` 位置索引断言改为**按 id 查找**
543
+ ——一条通知从放行转丢弃会让其后索引整体错位,那种红是索引级联不是语义翻面,会误导人去改产品码。
544
+
31
545
  ## 0.44.0(2026-08-26)
32
546
 
33
547
  **批面**:server 7.48.0 消费批四件(全 additive;实现以 **7.48.0 fixture dist 真字节**为准,
@@ -60,7 +574,7 @@
60
574
  additive 补键+帧→卡透传;`AskDecisionBody` 是 `.strict()` 且无 `persistRule` 键 ⇒
61
575
  **不镜像**该键防假 affordance。
62
576
 
63
- **测试**:32 门绿;codex 两轮 6 findings 5 修 1 登记;8 变异全抓红(含条数一致门关停变异)。
577
+ **测试**:32 门绿;复审 两轮 6 findings 5 修 1 登记;8 变异全抓红(含条数一致门关停变异)。
64
578
 
65
579
  ## 0.43.1(2026-08-25)
66
580
 
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.44.0
38
+ **Version:** 0.46.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
@@ -230,6 +230,7 @@ public-surface guard checks that last one).
230
230
  | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise |
231
231
  | `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal) |
232
232
  | `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
233
+ | `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
233
234
  | `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
234
235
  | `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
235
236
  | `scripts/run-limits-env-failloud-test.mjs` | `SEMA_HEADLESS_*` env-lane limits reject invalid values as loudly as the flag lane (no silent "no budget" runs) |
@@ -293,6 +293,38 @@ const engineNoticeArm = function* (m) {
293
293
  ...(typeof m.eventSeq === 'string' && m.eventSeq.length > 0 ? { eventSeq: m.eventSeq } : {}),
294
294
  });
295
295
  };
296
+ /**
297
+ * `text_end` 内部臂 → chrome `text_segment_end`(#323 / core #447)。
298
+ *
299
+ * 契约本体在 `seam.ts` 的 {@link TextSegmentEndChromeEvent} 头注(它是宿主要读的那一份)。
300
+ * 本臂只做三件:
301
+ * 🔴 **子流断闸**(与 `engine_notice`/`prompt_suggestions`/thinking 半场同族):带 `parentToolCallId`
302
+ * 的段边界属于子代/编排流,上 leader 面就是跨 lane 状态破坏 —— 直接不产事件。
303
+ * 🔴 **不动文本缓冲**:本批**不**在这里调 `takeAnswerSegment()`。撤 idle-flush 启发式要先按
304
+ * core 的「诚实缺席」纪律做一条 **per-stream** 的判据(整条流一帧都没有才回落启发式),
305
+ * 那是带状态的行为面改动,按宪法三问单独走。本批只把边界送到宿主手上。
306
+ * · 第二道 `content` 在场判(投影层已判 malformed/empty):同 `engine_notice` 的 carrier 二道判,
307
+ * 防的是**非投影口喂进来的帧**(宿主自建管线 / 重放存量转录),不是重复判据。
308
+ */
309
+ const textSegmentEndArm = function* (m) {
310
+ // 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
311
+ // 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
312
+ // 重放存量转录)兜底 —— 那里若沿用同族其它臂的 `typeof === 'string'` 写法,一个坏值就会把
313
+ // 子代的段边界**擦成 leader 的**上到宿主面。同族臂的 `typeof` 写法在它们那里只影响一行装饰,
314
+ // 在本臂上决定的是「这条边界算谁的」,方向必须更严。
315
+ // ⚠️ `null` 也算在场(不给它开口子):wire schema 只允许缺席或 string,`null` 是坏值不是缺席。
316
+ if (m.parentToolCallId !== undefined)
317
+ return;
318
+ const content = typeof m.content === 'string' ? m.content : '';
319
+ if (content.length === 0)
320
+ return;
321
+ yield chrome({
322
+ kind: 'text_segment_end',
323
+ laneProof: MAIN,
324
+ content,
325
+ ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
326
+ });
327
+ };
296
328
  const promptSuggestionsArm = function* (m) {
297
329
  // T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
298
330
  if (typeof m.parentToolCallId === 'string')
@@ -754,6 +786,7 @@ export const ARMS = new Map([
754
786
  ['workspace_changed', workspaceChangedArm],
755
787
  ['human_input', humanInputArm],
756
788
  ['engine_notice', engineNoticeArm],
789
+ ['text_end', textSegmentEndArm],
757
790
  ['prompt_suggestions', promptSuggestionsArm],
758
791
  ['retry_status', retryStatusArm],
759
792
  ]);
package/dist/adapt.d.ts CHANGED
@@ -50,7 +50,7 @@ export interface AdapterLedgerState {
50
50
  /** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
51
51
  export declare const ADAPTER_COVERAGE: {
52
52
  /** 已落码的帧臂。 */
53
- readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "human_input", "engine_notice", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
53
+ readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "human_input", "engine_notice", "text_end", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
54
54
  /** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
55
55
  readonly transcriptKinds: readonly ["assistant_text", "assistant_thinking", "assistant_tool_use", "user_tool_result_decision", "user_task_notification", "system_passthrough"];
56
56
  /** B3(0.5.0)新落码的臂/半场 —— 从 todo 移过来的,别再在 todo 里留同名条目。 */