@sema-agent/client-core 0.68.0 → 0.68.2
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 +206 -0
- package/README.md +4 -1
- package/dist/adapt/arms.js +44 -7
- package/dist/adapt/textSegmentAuthority.d.ts +157 -0
- package/dist/adapt/textSegmentAuthority.js +108 -0
- package/dist/adapt/textStream.d.ts +143 -1
- package/dist/adapt/textStream.js +246 -6
- package/dist/adapt.js +18 -1
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +19 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +18 -8
- package/dist/controlRouter.d.ts +16 -0
- package/dist/controlRouter.js +6 -0
- package/dist/engineHttpTools.d.ts +31 -0
- package/dist/engineHttpTools.js +30 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +20 -0
- package/dist/mcpPanel.d.ts +60 -0
- package/dist/mcpPanel.js +105 -0
- package/dist/request/taskRequest.d.ts +54 -0
- package/dist/request/taskRequest.js +75 -0
- package/dist/sdkWireTransit.d.ts +41 -0
- package/dist/sdkWireTransit.js +32 -0
- package/dist/seam.d.ts +86 -8
- package/dist/seam.js +16 -2
- package/dist/toolRoster.d.ts +34 -2
- package/dist/toolRoster.js +16 -2
- package/docs/INTEGRATION-CLIENTS.md +481 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,212 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.68.2(2026-09-15)
|
|
53
|
+
|
|
54
|
+
> 主题:**归层** —— 三件「本包一份、端各一份」的东西收成一份。都不是新能力,只是把同一句话
|
|
55
|
+
> 从几层里收回一层;wire 面零改动、既有行为逐字未变(判决归一、**落账不归一**)。
|
|
56
|
+
> 接入面详见 `docs/INTEGRATION-CLIENTS.md` **§35**(逐键处置表 §35z)。
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **`resolveTextSegmentAuthority(content, { committedPrefix, openSegment })`**(L-318)——
|
|
61
|
+
`text_end` 权威段替换的**六形判决单源**,纯函数、零宿主形。0.68.1 落的那份长在
|
|
62
|
+
`adapt/textStream.replaceAnswerSegment` 里(服务交互车道的双平面),而**自己缝 delta 成帧**的
|
|
63
|
+
车道(`-p` / web / desktop 座位层)拿不到那只段缓冲 ⇒ cli 1.0.114 在壳里逐形又写了一遍。
|
|
64
|
+
现在两边吃同一只:`form` 三支(`untouched` / `replace_tail` / `withhold`)+ 三位事实
|
|
65
|
+
(`diverged` / `committedPrefixLen` / `committedPrefixDiverged`)+ 尾段切片。
|
|
66
|
+
- **`createTextSegmentLedger()`**(L-318)—— print 形车道的**段账状态机**:已出门前缀跨工具卡
|
|
67
|
+
**累加**、三处清账(durable 取代 / 模型轮收口 / 终局 flush)、被扣段的终局**规范化后逐字**判重。
|
|
68
|
+
- **`memoryCaptureDeclarationField(declared)` / `MEMORY_CAPTURE_OFF`**(L-316)—— 会话级隐私声明
|
|
69
|
+
的**字面唯一铸点**:端交意图位(布尔),wire 值由本层铸。`false` ⇒ **整键缺席**(缺席就是
|
|
70
|
+
「照常采集」的唯一写法)。坏拼写仍由 `buildTaskRequest` **响亮拒**(静默删键会让 opt-out 无声丢失)。
|
|
71
|
+
- **`engineUrl` / `probeHealth`(+ 型 `ProbeHealthResult`)**(L-61 件④)—— SDK 纯工具面的
|
|
72
|
+
**原样转口**(同一函数引用,零包装)。只为取这两件而直连 `@sema-agent/sdk` 的端从此有「经包」的路。
|
|
73
|
+
- **`AgentClient` / `isApprovalRequestFrameV1` / `isApprovalRevokeFrameV1` + 一小撮 wire 型面**
|
|
74
|
+
(L-61 存量清零;clay 令 C-R43「所有欠账绝不延期,宁可红」)—— `src/sdkWireTransit.ts`,
|
|
75
|
+
**原样转口**。🔴 **改口记明**:`AgentClient` 在本档早先的处置表里是 `declined`(「转口一个客户端类
|
|
76
|
+
= 本包为它的连接姿势/鉴权头/重试语义背书」)。那条顾虑仍然成立,但它指向的是**设计问题**
|
|
77
|
+
(端该不该直接 new 一个客户端),而把它与**归层问题**(这条 import 该不该经本包)绑在一起,
|
|
78
|
+
代价是归层被一个未排期的设计件无限期挡住。⇒ 两件分开:归层现在做,设计件照旧欠着 ——
|
|
79
|
+
**新码一律走 `makeEngineWireClient`,本转口口只给存量用**(写在模块头注,不靠人记得)。
|
|
80
|
+
型面**只收端上存量实际用到的那些名字**,不是 SDK 全量镜像(射程与权威归属见 §35e)。
|
|
81
|
+
|
|
82
|
+
- **`SEMA_SEGMENT_ID_KEY`**(CC-01)—— committed assistant **文本**转录行顶层的段身份键 `_sema_segment_id`,本包在 `adapt()` 出口盖(durable 整条与流式分段两腿同盖;思考块 / 只带 tool_use 的行 / chrome 事件一个字节不动);`text_segment_end` 事件新带**必在** `segmentId`(轮换前的那个,与它盖过的行同值)。语义:一个身份覆盖「上一次段收口 → 本次段收口」之间过境的全部 committed 文本行;同流重放同身份。此前由壳在自己的过境口自铸(先壳后包),本包 diff 门对壳树当场红 —— 铸点归包,壳只读。接入面 §35f。
|
|
83
|
+
- **`projectMcpPanel(body)` / `mcpPanelLastLegDetail(view, { reachable })`**(CC-03)—— `GET /v1/sessions/:id/mcp` 面板体的防御读视图 + `lastLegMcp` 一行措辞唯一铸点(server ≥7.77.0 可选键;缺席 = 键不铸、在场读不懂 = `lastLegMcpUnreadable: true`;`mcp[]` 与 `wiring_manifest` 第三段同一只读器;禁与 `servers[]` 对账)。`projectMcpSection` 随之转公面。接入面 §35g。
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- `adapt/textStream.replaceAnswerSegment` 的六形判据改调判决单源;**落账一行未动**
|
|
88
|
+
(handOffOnly / 活体尾巴重算 / 封存 / 终答补差账同步)。两条车道的账本按不同边界切
|
|
89
|
+
(transcript 消息 vs CC 帧),归一落账 = 改掉其中一条的既有行为,刻意不做(§35z 第 3 行 declined)。
|
|
90
|
+
|
|
91
|
+
- `TextSegmentEndChromeEvent` 新增必在键 `segmentId`(additive;既有三键与义务一字未变);臂表义务文案点名「两把钥匙」。
|
|
92
|
+
- diff 门:`_sema_segment_id` 与 `uuid` 同族按结构别名归一(壳侧随机铸 vs 包侧确定性派生;壳退成只读后自然零触发)。
|
|
93
|
+
|
|
94
|
+
### Guards
|
|
95
|
+
|
|
96
|
+
- 新门 `scripts/run-segment-authority-single-source-test.mjs`(55 条):六形正控 + 「锚在
|
|
97
|
+
`startsWith` 不在『有没有 flush 过』」负控 + UTF-16 单位(对非 ASCII 样本,其 UTF-8 字节数不同)
|
|
98
|
+
+ 与真 `textStream` 的同源钉 + 段账三形 + 字面铸点端到端 + 两个转口口都按**同一函数引用**断言(不是「能调通」)+ 审批帧谓词与 SDK 自己那一只对同一帧同答案(两向都验);⑥ 段用 **TS parser
|
|
99
|
+
剥注释**后做**第二算式反钉**(行为面对「有没有第二份真相」零判别力 —— 两份独立实现对同一输入
|
|
100
|
+
本来就会给同一答案)。
|
|
101
|
+
⚠️ 与 0.68.1 的 `run-text-segment-authority-test.mjs`(守**交互车道的行为**)**分工不同,别合并**。
|
|
102
|
+
- `run-client-core-portability-test.mjs` 的两个棘轮按既有姿势逐件记账上调:A 层闭包 25→26
|
|
103
|
+
(`adapt/textSegmentAuthority.ts`,零 import 纯叶)、index 闭包 159→162(再加 `engineHttpTools.ts`
|
|
104
|
+
与 `sdkWireTransit.ts`;`EXPECTED_PACKAGES_INDEX` **一字不动** —— SDK 本来就在集合里,且 SDK 的
|
|
105
|
+
`dist/` 全树实测零 `node:` 内建)。
|
|
106
|
+
|
|
107
|
+
### 已知局限(本版新增)
|
|
108
|
+
|
|
109
|
+
- **中途翻转 verb** `POST /v1/runs/:id/memory/capture-optout`(L-286②)仍是包侧缺口:七种 typed
|
|
110
|
+
结局的处置表是三端公共判定、归本包,本批只收了**声明形**的铸点。
|
|
111
|
+
- 🔴 **`@sema-agent/core` 的值级面仍然没有「经包」的路**(cli `core-value` 桶 4 条):core 的 barrel
|
|
112
|
+
值级拉 `node:crypto`/`node:fs`/`node:path`,而本包的可移植门是外部包**等值集** + `--platform=browser`
|
|
113
|
+
真打包 ⇒ 值级 re-export 会把整台引擎焊进 web/desktop 产物。唯一可用形是**端口注入**
|
|
114
|
+
(`hitl/editedRuleTextPrecheck.ts` 先例),但它要求宿主有一个**可登记的装配根** —— 那是端侧决策,
|
|
115
|
+
本包单方面解不了。逐条需要的端口签名见 cli 车 ZF 回执。
|
|
116
|
+
- **`memoryCapture` 的求值时刻属端、而且不能随便挑**:它是**时效键**(会话级、单向、不可逆),
|
|
117
|
+
端必须在「决定这一发提交到哪条会话」的**同一拍**求值。把它提前到请求构造那一刻会在等待窗
|
|
118
|
+
(就绪门 / 退避 / 后台请求跨 `/resume`)里把 A 的声明写到 B 头上 —— 本批异源对抗复审 r2 [high]
|
|
119
|
+
在 cli 上实复现过。本包只负责「值由谁铸」,**不**替端决定「什么时候铸」;§35d 已把这一条写进用法。
|
|
120
|
+
|
|
121
|
+
## 0.68.1(2026-09-15)
|
|
122
|
+
|
|
123
|
+
🔴 **内容批**(零上游换钉:devDep / peer 地板一行未动)。逐件的铸点读法 / 缺席语义 / 三端升级必读三条 /
|
|
124
|
+
黑盒判据 G34-01…13 / 逐键处置表见 `docs/INTEGRATION-CLIENTS.md` **§34**(母本 §34z)。
|
|
125
|
+
|
|
126
|
+
🔴 **本批最该记住的一句话**:`text_end.content` 与 `text_delta` 从「逐字节相等」变成「**可以不相等**」
|
|
127
|
+
(server 7.75.3 起前者经脱敏器、后者仍逐字)。同一段话换了两种字节之后,本包此前那句「拿它再渲一行 =
|
|
128
|
+
同一段文字上屏两遍」就从**对的**变成了**危险的** —— 按它不渲,留在本地转录里的是**未脱敏**那一份。
|
|
129
|
+
|
|
130
|
+
### L-310 🔴 `text_end` 权威段替换(安全面)
|
|
131
|
+
|
|
132
|
+
- `adapt/arms.ts` 的 `textSegmentEndArm` **改口**:修前头注自陈「本批**不动**文本缓冲」,现在它调
|
|
133
|
+
`adapt/textStream.ts` 新增的具名动作 `replaceAnswerSegment(content, frame)` —— **整段替换**段缓冲与
|
|
134
|
+
活体尾巴,再发 chrome 信号(替换**在**发信号之前:反过来会留一拍窗口,宿主按信号去读转录面读到的
|
|
135
|
+
还是旧拼文)。臂的签名随之从 `ProjectionArmFn` 改成 `ArmFn`(封闭性声明就是签名,改签名在 diff 里显形)。
|
|
136
|
+
- chrome `text_segment_end` 新增**三个 additive 键,全部 never-false**(缺席 = 否定,与 `_sema_usage_absent`
|
|
137
|
+
同律):`diverged`(活体面那份过期了 ⇒ 重渲)、`committedPrefixDiverged`(转录面已 committed 的那一截
|
|
138
|
+
**也**过期了 ⇒ 丢掉它们并用 `content` 重渲;包在这一形上**一个字节都不再交**)、`committedPrefixLen`
|
|
139
|
+
(丢到哪的定位量;按**增量拼文**计长,不是 `content` 上的偏移)。🔴 **b1/b2 两形靠第二个键分**:另两个键
|
|
140
|
+
在两形上完全同形,而宿主动作正相反,所以合成一个位办不到。刻意**没有**第四个键 `authoritative: true`
|
|
141
|
+
(每帧恒真 ⇒ 零判别力)。
|
|
142
|
+
- 🔴 `CHROME_ARM_TABLE` 上本臂的 `required` **`false` → `true`**:本表的 `false` 语义逐字是「不接 = 这条
|
|
143
|
+
披露看不见,不属『已发生的行为丢失』」,而现在不接的后果是**未脱敏字节留在本地转录里**(那条明文消息
|
|
144
|
+
**已经渲过**)。留 `false` = 把一条安全面义务标成可选。按本表自检覆盖率的端会当场显形,那正是目的。
|
|
145
|
+
- **替换语义六形**逐形处置(判据锚在 `content.startsWith(<已提交前缀>)`,不锚「有没有 idle-flush 过」):
|
|
146
|
+
(a) 常态整段换;(b1) 半段已 flush 且前缀仍对得上 ⇒ 只封存未提交尾段,两条 committed 拼起来逐字节 =
|
|
147
|
+
`content`;(b2) 前缀自己也过期 ⇒ **一个字节都不再交** + 立 `committedPrefixDiverged`(**包内撤不回**:
|
|
148
|
+
那条 transcript 消息早已 yield,只有宿主持有句柄;再补一条 = 同一段话上屏两遍);
|
|
149
|
+
(c) 🔴 一 turn 多段 ⇒ 定稿正文**封存**、段缓冲清空给下一段用;(d) 子流断闸**在替换之前**;
|
|
150
|
+
(e) 🔴 **迟到的段边界**(含卡后同段继续出 delta)⇒ **两本前缀账相加**后照算分歧照发信号;
|
|
151
|
+
(f) 🔴 **本包零经手这一段** ⇒ **什么都不做**。
|
|
152
|
+
- 🔴 **(c)(e) 两形是异源对抗复审当场实抓的真病**(两条都对着装机 core 的真字节复现),第一版设计两条都错:
|
|
153
|
+
· (c)【anthropic 车道常态,`brain/anthropic.js` 在 `content_block_stop` 上逐块 push `text_end`】
|
|
154
|
+
第一版把 `content` 写回段缓冲而不清空 ⇒ 第二段的 delta 接着往同一个缓冲里加、再被第二帧的 `content`
|
|
155
|
+
整段覆盖 ⇒ **第一段正文凭空消失**,且第二帧还误报 `diverged`。修形 = 定稿正文移进「封存」缓冲,
|
|
156
|
+
段提交本体交 `封存 + 当前段`;**分段行为零改动**(整 turn 仍只提交一条 assistant 文本消息)。
|
|
157
|
+
· (e)【openai 车道实测时序,`brain/openai.js` 在 `finalize()` 里 push `text_end`,而 `toolcall_end` 先触发
|
|
158
|
+
`engine/loop/agent-loop.js` 的 `executor.maybeAdmit` ⇒ 工具卡先到、段边界后到】第一版按「两个缓冲都空
|
|
159
|
+
= 没经手过」全否返回 ⇒ **一段未脱敏正文静静留在转录里、三个键一个都不发**。修形 = 记下「上一个包侧
|
|
160
|
+
边界带走的那一截」,迟到的边界据它照算分歧、照发三个键,但**不铸消息**(字节在上一条消息里)。
|
|
161
|
+
· 两条都带常驻回归格,且 (c) 的格**刻意不手动插段提交**(手插正是第一版测试掩盖该病的原因)。
|
|
162
|
+
- 🔴 **复审轮二又抓出两条组合时序**(同样对着装机 core 的真字节复现,两条都在轮一修法之上):
|
|
163
|
+
· 工具卡**之后同段还有 delta**(brain 累加到 finalize + streamingToolExecution)⇒ 轮一的
|
|
164
|
+
「只在当前缓冲全空时才读上一本前缀账」当场失效:卡前那截明文**不算进前缀** ⇒ 两个前缀键双双
|
|
165
|
+
缺席、旧明文消息留存,而权威全文又被**完整提交一次**。修形 = **两本前缀账相加**
|
|
166
|
+
(包侧边界带走的 + 本消息内 idle-flush 掉的),「当前缓冲空不空」不再参与判据。
|
|
167
|
+
· `committedPrefixLen` 的**单位**此前写成「几条消息」⇒ `A → text_end(A) → B前缀 → idle-flush →
|
|
168
|
+
B尾 → text_end(B)` 时包把 A 的定稿与 B 的半截合并进**同一条**消息,宿主按消息丢会连 A 一起
|
|
169
|
+
删掉、而包不会再补发。修形 = 契约改成**字节区间**(从已 committed 正文尾部往回数),
|
|
170
|
+
三处宿主义务文案(`seam.ts` 义务① / `CHROME_ARM_TABLE` duty / §34)同批改齐。
|
|
171
|
+
· 轮二第三条 [medium]:G34-04 与臂注仍写「随后那条 committed 消息 = 整段 content」,与实现
|
|
172
|
+
(该位为真时**不交正文**)相反 —— 同批改口,并把验收判据从「三个键在不在」改成**断言最终转录**。
|
|
173
|
+
- 🔴 **复审轮三再抓出两条**(同样先自己复现再改;复现脚本 `repro3.mjs`):
|
|
174
|
+
· `committedPrefixLen` 的**单位**此前写成「字节」,而实现返回的是 `String.length`(**UTF-16 代码
|
|
175
|
+
单元**)⇒ 宿主照文案按 UTF-8 字节去截,在任何非 ASCII 正文上都会截错位置:实测
|
|
176
|
+
`'sk-…' + '中文'×30` 的 UTF-16 长度 82 / UTF-8 字节 202,按 202 截**凭据原样留在屏上**还附带乱码。
|
|
177
|
+
修形 = 文案正名成「JS 字符串长度(UTF-16 代码单元)」并给出可照抄的那一行
|
|
178
|
+
`已committed正文.slice(0, 长度 - committedPrefixLen) + content`;**实现一字未改**(它本来就对),
|
|
179
|
+
错的是三处宿主义务文案。门用真实 CJK 前缀把两条重建路径都跑一遍,反向自证「按字节截的后果」。
|
|
180
|
+
· `committedPrefixDiverged` 那一形**没同步终答补差账**:包不交正文,但 `committedAnswerText` 还留着
|
|
181
|
+
旧的那一份 ⇒ `result` 臂把权威全文当成「屏上缺的后缀」**再渲一遍**(第④臂),正好违反三处文案
|
|
182
|
+
承诺的「不再交正文」。修形 = 这一形下同批把补差账里属于本段的那一截换成 `content`
|
|
183
|
+
(「宿主照契约换过之后屏上有什么」),于是补差落「两份是同一段话」那一臂、什么都不补。
|
|
184
|
+
- 🔴 **复审轮四把上一条的射程收窄**(两条 finding,都先复现再改):同步**只在「整截都在当前这条
|
|
185
|
+
assistant 消息里」**那一形做。段跨过包侧边界(工具卡 / 消息划界)时那一截的字节在**别的**消息里,
|
|
186
|
+
而两本补差账是**按消息**记的 —— 往当前那本写就是把别条消息的正文算进这条:实测 ① 迟到边界那一形
|
|
187
|
+
会让随后正常的段被误报 `result_text_diverged`、甚至把该补的后缀压掉;② 同段跨两张卡时
|
|
188
|
+
`previousCommittedText` 只留得下最后一条,守卫不成立、同步静默跳过。⇒ 跨消息那几形**不碰账**。
|
|
189
|
+
🔴 **代价如实认领,且本批之前就有**:那里的补差基线与「宿主换过之后的屏」对不齐 ——
|
|
190
|
+
主树 0.68.0 上同一条时序**实测同形**(卡后有散文时基线取的就是卡**之后**那一条,而终帧是整条消息
|
|
191
|
+
的全文)。根因 = 包按自己的边界切 committed 消息、引擎按 content block 切段;根治 = 「每个引擎段
|
|
192
|
+
各自一条 committed 消息」(CC 原生做法),**分段行为改动,单独走**。四条射程格 + 反向自证入门。
|
|
193
|
+
- **活体面**:已泄前缀仍是 `content` 前缀 ⇒ 尾巴按替换后的段重算;不是前缀 ⇒ **不再补尾巴**
|
|
194
|
+
(绝不在一段错的前缀后面接上权威后缀,拼出一段谁都没说过的话)。
|
|
195
|
+
- 文案同批改口三处:`seam.ts` 的 `TextSegmentEndChromeEvent` 义务①、
|
|
196
|
+
`adapter/downstream/eventToSdkMessage.ts` 的 `textEndProjection` 臂注(那句「与 delta 拼接逐字节相等」)、
|
|
197
|
+
`adapt.ts` 臂名表的括注。
|
|
198
|
+
- 📋 **仍然留白**:撤掉 idle-flush 启发式要按**每条流**判「这条流带不带边界帧」,属行为面改动,单独走。
|
|
199
|
+
权威替换是**兼容**它的,不是它的继任。
|
|
200
|
+
- 🆕 常驻门 `scripts/run-text-segment-authority-test.mjs`(五形 + 活体两形 + 三键 never-false +
|
|
201
|
+
b1/b2 分水岭 + 端到端一趟)。
|
|
202
|
+
|
|
203
|
+
### L-290 `ToolRosterEntryView` 出身三键(additive)
|
|
204
|
+
|
|
205
|
+
- 补 `contentOriginProvenance`(四词:`declared`/`server`/`exempted`/`default`)、`effectProvenance`、
|
|
206
|
+
`egressProvenance`,三者**开集读**、缺席即缺席、坏形只丢那一格不丢行。
|
|
207
|
+
🔴 **缺席 ≠ `"default"`**:缺席是「这台引擎没报」,`default` 是「报了,谁都没声明」。
|
|
208
|
+
- 🔴 **同形族扫**:病形 = 「逐键窄读器对上游已透传的键静默无感」(server 那一段是**整只判形 + 逐字透传**);
|
|
209
|
+
同形存量 = core `ToolRosterEntry` 32 个成员里本包此前只挑 17 个。处置不是只补被点名的那一个 ——
|
|
210
|
+
同族三键一起挑,并把族扫落成机器账:`run-tool-roster-projection-test.mjs` **G 段**拿**实装 core 的
|
|
211
|
+
schema 成员表**与窄读器真挑出来的键集**双向对账**,12 条 declined 逐条写理由,**上游再加成员当天红**。
|
|
212
|
+
- ⚠️ 出身键是**诊断面**,刻意**不进** `ToolShim`(渲染面 vs 诊断面分家)。
|
|
213
|
+
|
|
214
|
+
### 🔴 本件的总不变量(三轮异源对抗复审逼出来的那句话)
|
|
215
|
+
|
|
216
|
+
**本包只在「整段都在自己手里(同一条 committed 消息内)」时改自己交的字节;段一旦跨过包侧边界
|
|
217
|
+
(工具卡 / 消息划界),就只发那三个键、不动任何既有行为** —— 转录逐条、终帧补差走向都与 **0.68.0
|
|
218
|
+
逐字相同**(常驻门按「补差臂走向」这个真正决定结果的量做基线对照)。轮五实测的反例正是踩了这条:
|
|
219
|
+
在跨消息那一形清掉未提交尾段,会让 `result` 从第⑤臂(只报一行分岔)改成第④臂(**补吐一整段**),
|
|
220
|
+
那是**新增**的重复转录路径,不是「更安全」。⇒ 随之而来的留白(那一截可能落在别条消息里、甚至夹着
|
|
221
|
+
一张工具卡;补差基线也不跟着换 —— 它本来就不跟)如实写进 `seam.ts` 义务① 与 §34,根治 =
|
|
222
|
+
「每个引擎段各自一条 committed 消息」(CC 原生做法),**分段行为改动,单独走**。
|
|
223
|
+
|
|
224
|
+
### [7226] 包侧缺口 ①⑤
|
|
225
|
+
|
|
226
|
+
- 🔴 **`ControlSafetyCode` 加员 `blocked_by_hook`**(`steering.blocked_by_hook`,422:部署的
|
|
227
|
+
`userPromptSubmit` 门拦下,block/超时/崩溃同码 fail-closed)。输入**未被受理**(无 `human_input` 帧、
|
|
228
|
+
`inputId` 不入账)⇒ 处置 = **改内容自由重试**。修前它落开集兜底位 `steering_other`,而那一位的判词
|
|
229
|
+
是「别按成员猜它的意思」—— 把一条能自救的拒绝渲成一条不知道怎么办的拒绝。
|
|
230
|
+
⚠️ 端上按这个联合做**穷举 switch** 的地方会编译期红(没有 `default` 臂的话),这是设计。
|
|
231
|
+
- `REQUEST_FIELD_MATRIX` 补 `memoryCapture` 行 + `TaskRequestInput.memoryCapture?: 'off'` + 构造器
|
|
232
|
+
`=== 'off'` 才 stamp(**单成员闭集**,没有 `"on"`)。只补表不补构造器 = 端还得在唯一构造口外面
|
|
233
|
+
手加一行,所以三件同批。
|
|
234
|
+
- 🔴 **坏值响亮拒**(异源对抗复审轮五 finding① 采纳):`'on'` / `'OFF'` / 空串 / 布尔 / 对象等一切
|
|
235
|
+
表外写法 ⇒ **抛 `TypeError` 拒绝构造请求**(两条车道、**不受 live 门**;错误文本只报形状不回显值)。
|
|
236
|
+
修前是「不是 `'off'` 就整键不 stamp」—— **静默删键**,而删掉的恰是一条**隐私声明**:一个把
|
|
237
|
+
`/memory-capture off` 打成 `OFF` 的会话,请求照发、引擎照常采集,**没有任何人会知道**。
|
|
238
|
+
装机 core `dist/core/memory.d.ts` 的 `capture?: "off"` 头注逐字写着「Any other value … is REFUSED
|
|
239
|
+
loudly (`config.memory_capture_spelling`) … **a privacy request must not be dropped by a typo**」,
|
|
240
|
+
server 契约 §12.4 同样点名「把一条隐私请求按打字错误静默丢掉恰是**禁的方向**」⇒ 提前删键 =
|
|
241
|
+
把上游那道响亮门绕过去。与本文件 `resolvedSnapshotForWire` 对畸形权限快照的处置同一条纪律。
|
|
242
|
+
⚠️ `undefined` / `null` 仍是**合法缺席**(端没有这个入口 / 没声明),不拒不 stamp。
|
|
243
|
+
⚠️ **端要看这一条**:若曾把用户输入直接透给这一位,打错字现在会抛(这是设计)。
|
|
244
|
+
|
|
245
|
+
### 已知局限(本版新增)
|
|
246
|
+
|
|
247
|
+
- `committedPrefixDiverged` 那两形(b2 / 迟到边界)包内**撤不回**已 yield 的 transcript 消息,也**不再补交**
|
|
248
|
+
这一段的任何正文 ⇒ **不消费该键的宿主**在转录里只剩未脱敏的那半截、且缺后半段。这是如实认领的代价:
|
|
249
|
+
再补一条会让同一段话上屏两遍,而该键是 never-false 的显形键 + 本臂已翻 `required: true`。
|
|
250
|
+
- **迟到边界**那一形的 `committedPrefixLen` 指的字节区间可能落在**上一条** assistant 消息里,甚至
|
|
251
|
+
**中间夹着一张工具卡**(工具卡先到、段边界后到)。宿主按同一条规矩换;本包**不**替它决定新正文落在
|
|
252
|
+
哪条消息上(在这里补只会落到工具卡之后)。🔴 **根治办法是「每个引擎段各自一条 committed 消息」**
|
|
253
|
+
(= CC 在 `content_block_stop` 上的原生做法),那是**分段行为改动**、与撤 idle-flush 启发式同一件事,
|
|
254
|
+
按宪法三问单独走 —— 本批如实留白,不假装没有。
|
|
255
|
+
- 终帧补差在上述两形下可能落既有的第⑤臂 `result_text_diverged`(两份互相都不是对方前缀)——那是**诚实结局**
|
|
256
|
+
(只交长度位、不复述正文、不补吐),不是回归。
|
|
257
|
+
|
|
52
258
|
## 0.68.0(2026-09-13)
|
|
53
259
|
|
|
54
260
|
🔴 **BREAKING 提货批**(core **7.16.0 → 7.17.1**;三件破坏性 + 一次**引擎地板抬升**到 core ≥7.17.0)。
|
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.68.
|
|
38
|
+
**Version:** 0.68.2
|
|
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
|
|
@@ -292,6 +292,7 @@ public-surface guard checks that last one).
|
|
|
292
292
|
| `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
|
|
293
293
|
| `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise |
|
|
294
294
|
| `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 — the register is occupied again — this time by the rule-store-unreadable key the engine now defines, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption; its predecessor left the register the other way, by being retired upstream rather than by the anchor catching up) |
|
|
295
|
+
| `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back |
|
|
295
296
|
| `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
|
|
296
297
|
| `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 |
|
|
297
298
|
| `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 |
|
|
@@ -305,6 +306,7 @@ public-surface guard checks that last one).
|
|
|
305
306
|
| `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, nine words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the gate parses the members straight out of the installed SDK's declarations and reconciles them against the package's tables in both directions, so a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because the sets differ in kind: one is genuinely closed on the wire (an out-of-set record is withheld by the engine, so reading one means the record is damaged) while the other is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine) Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor |
|
|
306
307
|
| `scripts/run-engine-identity-test.mjs` | The engine generation anchors on `/health` (`pid`, `instanceId`, `startedAt`; engine >=7.67.0). `/health` is the one unauthenticated door and its heartbeat is always green, so "another host restarted the shared engine" used to be discoverable only by having some authenticated request hit a 401 first — a path that misreads a restart as a network fault. The reader narrows each anchor independently (one malformed field never hides the other two) and always hands back a reading object rather than an absence, because the caller is asking which anchors answered, not whether there was a response. The comparison is a three-word verdict, not a boolean: `unknown` when the two readings share no comparable anchor at all — an empty intersection means nothing could be compared, never that nothing changed — and the boolean convenience is pinned so that only `true` is an assertion. Any comparable anchor differing decides `changed`, so a reading whose `startedAt` matches while its `instanceId` does not cannot be waved through as the same life; precedence only decides which anchor gets named in the diagnosis |
|
|
307
308
|
| `scripts/run-posture-knob-projection-test.mjs` | The three deployment knobs on the operator face (`serverGates.durableApproval` / `streamAskWindowMs` / `sessionAutoTitle`, engine >=7.67.0), each read as a value **plus who set it plus one operator-facing pointer** rather than a bare value — a bare boolean cannot answer why this particular machine is on this setting or how to pin it back, and a default that flips with the deployment shape is invisible without that. A worker too old to report readings still sends a bare boolean; the reader folds it into the same shell so consumers keep one branch, but raises a `legacy` bit, answers `undefined` from the machine-readable source accessor, and mints a sentence that contains no source word at all — claiming a source nobody reported is worse than admitting the worker cannot say. The other two knobs are honestly absent on such a worker rather than defaulted, a malformed side knob drops only itself while the anchor knob drops the whole reading, and the four sentences are pinned literally distinct so an operator can tell "not observed" from "not reported" from a real value. The last leg reads the installed SDK's `openapi.yaml` and `types.d.ts` directly, including a pin that exactly one knob on this face is numeric — the premise the millisecond-to-prose rendering rests on |
|
|
309
|
+
| `scripts/run-mcp-panel-projection-test.mjs` | The `GET /v1/sessions/:id/mcp` panel reader (`projectMcpPanel`; server >=7.77.0 adds the optional `lastLegMcp` key) and the single wording mint for its "last leg" line. Absence of `lastLegMcp` is one literal sentence that never blames the engine version (a new session, a leg outside the retention window, a leg without a manifest and an older engine all look the same on the wire); a key that is present but unreadable is a different sentence plus a `lastLegMcpUnreadable: true` mark, never folded into absence. The `mcp[]` roster goes through the same reader as the live `wiring_manifest` third section, so a replayed roster and a live one have one shape. The two faces of the panel (`servers[]` and the last-leg roster) may legitimately differ, so the view carries no agreement flag and none of the five sentences mentions `servers`. Required keys are pinned to the SDK `openapi.yaml` component bytes |
|
|
308
310
|
| `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
|
|
309
311
|
| `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
|
|
310
312
|
| `scripts/run-seat-task-request-origin-test.mjs` | Where every field of the seat lane's send-message payload comes from, and whether it actually lands anywhere. The seat payload is a closed interface this package mints itself, and most of its fields are meant to ride verbatim onto the engine's request body — two facts nothing used to connect, so both directions could drift in silence. A seat field could be named after a request position that does not exist, in which case a client writes to it, the wire carries it, the engine ignores the whole key, and the screen shows a switch that does nothing; conversely a new request position could arrive with no seat to sit in, which is **structural** absence — the closed set *is* the carrier, so a decision missing from it has nowhere to be put at all, the same shape logged when the effort dial had no seat. The guard turns each field's origin into data: either it names the request position it forwards to, or it is declared seat-local with a written reason, and the two are mutually exclusive. Forwarding claims are then checked against the **installed** SDK's type declarations, parsed rather than restated — a hand-copied list of position names would only ever prove that two transcriptions agree. The parser is held to reading top-level positions only, since a nested option object's inner keys would otherwise be mistaken for positions of the request itself, and it proves that discrimination on synthetic input before any verdict is given. The two subagent fields carry a standing regression pin, and the retention window's inner keys are read from the declaration the same way, so a seat that offers a tunable window cannot offer one the wire has no room for |
|
|
@@ -363,6 +365,7 @@ public-surface guard checks that last one).
|
|
|
363
365
|
| `scripts/run-cost-reconcile-projection-test.mjs` | The **end-of-run cost reconciliation** reaching consumers at all. The engine splits a run's spend on the wire — the task's own cost, which deliberately excludes delegated sub-agents, the delegated total itself, and the within-task compaction subtotal that sits inside the own figure — and states two reconciliation identities for them. The package used to project none of it, so a cost view could only ever see one number and under-reported both delegated and compaction spend. Both structures are now projected onto the result as superset fields in the wire's integer micro-currency unit, read key by key, with unreadable keys dropped individually, an entirely unreadable structure omitted rather than emitted empty, and unknown categories passed through since the vocabulary belongs upstream. The delegated cost stays **absent when it was never priced**, never a fabricated zero. The same reader also feeds a terminal chrome arm carrying the three parts plus the reconciled total, so the two faces can never compute different answers; the reconciled total is minted only when both sides are known, and otherwise a discriminator bit says which side is unknown. **The reference field for total cost keeps its meaning** — it remains the task's own spend and the delegated total is not folded into it — because that is a shape the wider ecosystem reads; the reconciled figure is offered beside it, not in place of it. A frame that carries no stats emits no arm at all, and the existing rule that in-stream per-turn usage is not published for sub-flows is pinned unchanged, since delegated spend arrives once, at the end. The bit that says those figures are a lower bound is **per stream**, not per context: the emit context belongs to the caller and may be reused across streams, so a gap observed on one run is no evidence at all about the next one — the observation is held for the duration of one stream and handed to both projection faces by value, and the guard drives a reused context both sequentially and concurrently to prove neither direction leaks |
|
|
364
366
|
| `scripts/run-task-progress-terminal-projection-test.mjs` | The one tick that says a delegated child **finished**. The engine fires exactly one final beat carrying a terminal face, and says in the same breath why it exists — so a consumer sees the row finish instead of watching it vanish after the last running beat — but the package's projection whitelist had no seat for that field and its adapter still carried the older premise in a comment, so the terminal beat arrived byte-identical to another running one: the panel row stayed up waiting for a defensive sweep (which only ever settles rows bound to a card still open this turn) or for a separate notification frame. The status now rides through as an **open set** with the vocabulary left upstream, while the question *which words are terminal* is answered by a closed pair on the adapter side — an unrecognised new word takes the running path, because guessing it terminal ends a row that is still working whereas one extra running beat merely renders late. A terminal beat settles the row directly under the lane proof its binding gives it (not the main lane a notification would use, and not by card id, since the engine is naming a child rather than closing a card), freezes the inline group-row twin in the same beat so a later sweep cannot reset the real tool count, clears the session-resident ledger, and fires the stop hook only for a child whose start really fired. It does not mark the row live or emit a second progress beat, and it shares the settled-row ledger with the other two settle legs so a replay or a double-delivery cannot produce a second end. Three things are pinned **unchanged**: a running beat, an absent status (older engines never send the field, and reading absence as terminal would make every child row disappear on its first beat), and the workflow lane gate, which still runs before any of this |
|
|
365
367
|
| `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
|
|
368
|
+
| `scripts/run-text-segment-authority-test.mjs` | The **authoritative segment replacement** on `text_end` (L-310, server >=7.75.3). `text_end.content` now goes through the same redactor as `result` and the ledger while `text_delta` stays verbatim, so the two **may differ** — an answer that quoted a credential used to be committed to the local transcript in its unredacted form, because the arm only forwarded the boundary signal. Six timing shapes are pinned, two of which an adversarial review reproduced against the installed engine's real bytes and which the first design got wrong in both directions: a second boundary in the same turn (the per-block case on one provider lane) used to make the first segment's prose vanish, and a boundary that arrives *after* the tool card (the other lane emits it at finalize) used to be read as "this package never handled that segment" and reported nothing at all. Three additive keys, all never-false; the two shapes that look alike are told apart by the second one, because the host's action in them is the opposite. The end-to-end legs drive the real pipeline without hand-inserting a segment commit — doing so is exactly what hid the first defect. A second review round then found two combination timings on top of the first fix — a tool card followed by *more* deltas in the same segment, and a byte count that had been documented as a message count — and both are pinned here too. A third round caught a length that the prose called bytes while the code returned UTF-16 units — harmless in ASCII, and on CJK text enough to leave the credential on screen — plus a backfill ledger that had to be kept in step, so the terminal frame does not re-render the segment a second time — kept in step only where the whole stretch sits in one message, because those ledgers are per-message and a fourth round showed that writing across them charges one message's prose to another. A fifth round settled the whole class into one invariant the guard now checks against the previous release's behaviour: this package only rewrites bytes it is still holding in the current message — once a segment has crossed a package-side boundary it emits the three keys and changes nothing else **0.68.2 (CC-01):** the segment identity is now minted here, not by the host: every committed assistant text row carries a top-level `_sema_segment_id` (stamped once at the `adapt()` exit, so the durable whole-message leg and the streamed-segment leg are covered alike; thinking blocks, tool_use-tailed rows and chrome events are left byte-for-byte), `text_segment_end` carries the same value as `segmentId` before rotating, subagent boundaries never rotate, and a replayed stream yields the same identities. Three mutations (no rotation / no stamping / stamping tool_use rows) each turn the guard red |
|
|
366
369
|
| `scripts/run-gate-negative-controls-test.mjs` | Whether the registry-shaped guards among the 74 suites above actually turn red when the material they check really breaks — a census had found 16 of them clean enough to rehearse safely (closed sets, mirrors, baselines, floors, a type-shape ratchet) without touching any judgement code. Each is exercised by tampering a disk copy of the real material, spawning the guard's own unmodified script, asserting it exits non-zero and names the disease, then restoring the file byte-for-byte. Seven guards of the same shape and 51 behaviour/projection suites are catalogued rather than rehearsed this round — see `docs/GATE-NEGATIVE-CONTROLS.md` for the full table, the reasons, and a one-minute manual replay recipe for each blind one. The suite cross-checks its own case count against that document's row counts in both directions, so a case quietly dropped from the array without the document following is itself an undeclared blind guard. The backup that makes the restore possible is taken by **exclusive create**: checking for it and then copying are otherwise two steps, and two instances can pass the check together — the later one overwrites the only clean copy with material the earlier one has already tampered, and the rehearsal that promises to leave no trace leaves a permanently corrupted file instead. That interleaving is rehearsed too, in a throwaway directory of its own |
|
|
367
370
|
|
|
368
371
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
package/dist/adapt/arms.js
CHANGED
|
@@ -407,19 +407,46 @@ const approvalFrameArm = (kind) => function* (m) {
|
|
|
407
407
|
yield chrome({ kind, laneProof: MAIN, frame, schemaVersion });
|
|
408
408
|
};
|
|
409
409
|
/**
|
|
410
|
-
* `text_end` 内部臂 → chrome `text_segment_end`(#323 / core #447
|
|
410
|
+
* `text_end` 内部臂 → **权威段替换** + chrome `text_segment_end`(#323 / core #447;L-310,
|
|
411
|
+
* server ≥7.75.3)。
|
|
411
412
|
*
|
|
412
413
|
* 契约本体在 `seam.ts` 的 {@link TextSegmentEndChromeEvent} 头注(它是宿主要读的那一份)。
|
|
413
|
-
*
|
|
414
|
+
* 本臂做四件:
|
|
414
415
|
* 🔴 **子流断闸**(与 `engine_notice`/`prompt_suggestions`/thinking 半场同族):带 `parentToolCallId`
|
|
415
|
-
* 的段边界属于子代/编排流,上 leader 面就是跨 lane 状态破坏 ——
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
416
|
+
* 的段边界属于子代/编排流,上 leader 面就是跨 lane 状态破坏 —— 直接不产事件,
|
|
417
|
+
* **也不动 leader 的文本缓冲**(断闸在替换之前,顺序不许倒过来)。
|
|
418
|
+
* 🔴 **整段替换文本缓冲**(0.68.1 改口;修前这里写的是「本批不动文本缓冲」)。改口的事实依据:
|
|
419
|
+
* server 7.75.3 起 `text_end.content` 与 `result`/账本走**同一只脱敏器**,而 `text_delta` 仍
|
|
420
|
+
* 逐字(跨 chunk 的凭据无法就地判)⇒ 两者**可以不相等**,而此前包内 committed 出去的是 delta
|
|
421
|
+
* 拼文 = **未脱敏**字节留在本地转录里。契约原文逐字要求消费端「在 `text_end` 到达时以它整段
|
|
422
|
+
* 替换已攒的 delta,而不是只当段界信号」。替换语义(四形处置 / 已提交前缀 / 活体尾巴重算)
|
|
423
|
+
* 全在 {@link TextStream.replaceAnswerSegment} 的头注。
|
|
424
|
+
* ⚠️ **这不等于撤掉 idle-flush 启发式**:那条要按 core 的「诚实缺席」纪律做 **per-stream** 判据
|
|
425
|
+
* (整条流一帧都没有才回落启发式),仍未做 —— 替换是**兼容**启发式的(半段已被 flush 那一形
|
|
426
|
+
* 由 `committedPrefixLen` / `committedPrefixDiverged` 如实交代),不是它的继任。
|
|
427
|
+
* 🔴 **三个 additive 键,全部 never-false**(缺席 = 否定;与包内 `_sema_usage_absent` 同律)。
|
|
428
|
+
* 铸一个 `false`/`0` 出来 = 把「这一段没分歧」与「这台引擎不报分歧」折成同一个字节,宿主分不出。
|
|
429
|
+
* **为什么恰是三个**(「宿主能不能只凭一帧就知道要不要重渲」的最小解):
|
|
430
|
+
* · `diverged` —— **活体面**那份(`stream_delta` 拼出来的)是不是过期的 ⇒ 要不要重渲;
|
|
431
|
+
* · `committedPrefixDiverged` —— **转录面**已经 committed 的那一截是不是过期的。它是真正的
|
|
432
|
+
* 分水岭:缺席时本臂随后交的 transcript 消息是**尾段**(前缀 + 尾段拼起来 = `content`,
|
|
433
|
+
* 宿主什么都不用丢);在场时本包**一个字节都不再交**(撤不回的那截拼不出 `content`,
|
|
434
|
+
* 再补一条就是同一段话上屏两遍)⇒ 该段唯一算数的那一份就是帧上的 `content`,
|
|
435
|
+
* 宿主用它换掉 `committedPrefixLen` 指的那一截。两个键合成一个位办不到:
|
|
436
|
+
* b1/b2 两形的 `diverged` 与 `committedPrefixLen` 完全同形,而宿主动作正相反。
|
|
437
|
+
* · `committedPrefixLen` —— **定位量**。单位 = **JS 字符串长度(UTF-16 代码单元)**,
|
|
438
|
+
* 🔴 **不是 UTF-8 字节**(轮三 finding① 订正:按字节截会在非 ASCII 正文上截错位置,凭据
|
|
439
|
+
* 原样留在屏上)、🔴 **也不是「几条消息」**(轮二 finding② 订正:本包在 idle-flush 那一形下
|
|
440
|
+
* 会把「上一段的定稿 + 这一段的半截」合并进**同一条**消息,按消息丢会把已经定稿的上一段
|
|
441
|
+
* 一起删掉,而本包不会再补发它)。宿主照抄
|
|
442
|
+
* `已committed正文.slice(0, 长度 - committedPrefixLen) + content` 即可。
|
|
443
|
+
* 📋 **刻意没有第四个键 `authoritative: true`**(设计取舍,写明免得下一棒再问):它会在**每一帧**
|
|
444
|
+
* 上恒为真 ⇒ 零判别力,既分不出新旧引擎(引擎版本不在帧上,本包不猜版本 —— 与 §33b 那条
|
|
445
|
+
* 「刻意不留按引擎版本分岔的兼容读」同一条纪律),也不驱动宿主的任何分支。
|
|
419
446
|
* · 第二道 `content` 在场判(投影层已判 malformed/empty):同 `engine_notice` 的 carrier 二道判,
|
|
420
447
|
* 防的是**非投影口喂进来的帧**(宿主自建管线 / 重放存量转录),不是重复判据。
|
|
421
448
|
*/
|
|
422
|
-
const textSegmentEndArm = function* (m) {
|
|
449
|
+
const textSegmentEndArm = function* (m, { text }) {
|
|
423
450
|
// 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
|
|
424
451
|
// 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
|
|
425
452
|
// 重放存量转录)兜底 —— 那里若沿用同族其它臂的 `typeof === 'string'` 写法,一个坏值就会把
|
|
@@ -431,12 +458,22 @@ const textSegmentEndArm = function* (m) {
|
|
|
431
458
|
const content = typeof m.content === 'string' ? m.content : '';
|
|
432
459
|
if (content.length === 0)
|
|
433
460
|
return;
|
|
461
|
+
// 🔴 替换在**发事件之前**:宿主拿到 `diverged` 那一刻,包内的转录面已经是权威全文了 ——
|
|
462
|
+
// 反过来(先发信号再改缓冲)会有一拍窗口,宿主按信号去读转录面读到的还是旧拼文。
|
|
463
|
+
const r = text.replaceAnswerSegment(content, m);
|
|
434
464
|
yield chrome({
|
|
435
465
|
kind: 'text_segment_end',
|
|
436
466
|
laneProof: MAIN,
|
|
437
467
|
content,
|
|
468
|
+
// CC-01:帧上带的是**轮换前**的段身份 —— 与本段已过境的 committed 行同值(归属的第二把钥匙)。
|
|
469
|
+
segmentId: text.segmentId(),
|
|
470
|
+
...(r.diverged ? { diverged: true } : {}),
|
|
471
|
+
...(r.committedPrefixDiverged ? { committedPrefixDiverged: true } : {}),
|
|
472
|
+
...(r.committedPrefixLen > 0 ? { committedPrefixLen: r.committedPrefixLen } : {}),
|
|
438
473
|
...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
|
|
439
474
|
});
|
|
475
|
+
// CC-01:段界到此为止 —— 事件已交到宿主手上之后才轮换(宿主按帧上的身份认行时,行上盖的还是同一个)。
|
|
476
|
+
text.rotateSegmentIdentity(m);
|
|
440
477
|
};
|
|
441
478
|
const promptSuggestionsArm = function* (m) {
|
|
442
479
|
// T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapt/textSegmentAuthority.ts — `text_end.content` **权威段替换**的判决单源(L-318,0.68.2)。
|
|
3
|
+
*
|
|
4
|
+
* ── 为什么有这个文件(病形一句话)────────────────────────────────────────────────────────────
|
|
5
|
+
* 0.68.1 把权威替换落在 {@link import('./textStream.js').TextStream.replaceAnswerSegment} 里,
|
|
6
|
+
* 那一只服务的是**交互车道**(transcript / chrome 双平面)。而 `-p` 车道走的是另一条链
|
|
7
|
+
* (`eventToSdkMessage` 的内部臂 → 宿主自己的 CC stdout 投影器),它拿不到那只段缓冲 ⇒ cli 1.0.114
|
|
8
|
+
* 在壳里**又实现了一遍同样的六形**(B-122 热修)。同一条语义两份实现 = 上游改口时两边各走各的:
|
|
9
|
+
* 这正是「壳持有第二份真相」那一族病。
|
|
10
|
+
*
|
|
11
|
+
* ⇒ 本模块把**判决**(六形里真正决定结果的那三件事实 + 尾段)抽成一只**纯函数**,两条车道同吃:
|
|
12
|
+
* · 交互车道:`textStream.replaceAnswerSegment` 调它,再按本包的双平面语义落账;
|
|
13
|
+
* · print 车道:宿主(cli `PrintStreamProjector`)调它 + {@link createTextSegmentLedger},
|
|
14
|
+
* 只剩「把尾段写进 CC content block」这一步装配动作留在端上(多端改造设计稿 §8-5:
|
|
15
|
+
* stream-json 帧序/SSE 重铸属端,判定属包)。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **本模块零宿主形**:进出全是 `string` / `number` / `boolean` —— 没有 CC content block、没有
|
|
18
|
+
* transcript 消息、没有帧。可移植门(`--platform=browser` 真打包)因此一个字节都不受影响。
|
|
19
|
+
*
|
|
20
|
+
* ── 判决与落账刻意分家(读这一段再改本文件)──────────────────────────────────────────────────
|
|
21
|
+
* 两条车道的**账本形状不同**,而且那个差别是有理由的,不许在这里归一:
|
|
22
|
+
* · 交互车道的已提交前缀分两本(`previousSegmentCommitted` / `segmentCommitted`),因为它按
|
|
23
|
+
* **transcript 消息**划界;段跨过消息边界、且已提交那截自己也过期时它**只发信号不动字节**
|
|
24
|
+
* (`handOffOnly`,0.68.1 轮五 finding② 的基线对照结论);
|
|
25
|
+
* · print 车道只有一本(工具卡边界带走的字节跨卡累加),没有那一形 —— 它的 stdout 是只进不退
|
|
26
|
+
* 的流,`(b2)` 恒走「一个字节都不再交」。
|
|
27
|
+
* ⇒ 本模块只交**判决**;「判决出来之后各自怎么落账」仍归各自那一层。把落账也归一 = 改掉其中一条
|
|
28
|
+
* 车道的既有行为,那是行为面改动,不是归层。
|
|
29
|
+
*/
|
|
30
|
+
/** 六形归到三种**处置**(宿主按 {@link TextSegmentAuthority.form} 分支,不按「有没有 flush 过」分支)。 */
|
|
31
|
+
export type TextSegmentAuthorityForm =
|
|
32
|
+
/** (d)/(f):本层一个字节都没经手这一段 ⇒ 什么都不做(零替换、零披露)。 */
|
|
33
|
+
'untouched'
|
|
34
|
+
/** (a)/(b1)/(c)/(e)-对得上:未提交的尾段整只换成 {@link TextSegmentAuthority.tail}。 */
|
|
35
|
+
| 'replace_tail'
|
|
36
|
+
/** (b2):已提交前缀自己也过期 ⇒ **一个字节都不再交**,由宿主按前缀长度换掉那一截。 */
|
|
37
|
+
| 'withhold';
|
|
38
|
+
/**
|
|
39
|
+
* 一次权威替换的判决 —— **四件事实**,一个字节的正文都不复述(`tail` 是权威全文的切片,不是复述)。
|
|
40
|
+
*
|
|
41
|
+
* 🔴 三位事实互不替代(L-310 设计定谳,0.68.1 头注逐字保留):
|
|
42
|
+
* · {@link diverged} 回答「活体/已发那一份对不对」⇒ 要不要重渲;
|
|
43
|
+
* · {@link committedPrefixDiverged} 回答「**已经撤不回**的那一截是不是过期的」⇒ 是换尾巴还是
|
|
44
|
+
* 一个字节都不交。两位合成一个办不到:b1/b2 两形的 `diverged` 与 `committedPrefixLen` 完全同形,
|
|
45
|
+
* 而消费动作正相反;
|
|
46
|
+
* · {@link committedPrefixLen} 是**定位量**。
|
|
47
|
+
*/
|
|
48
|
+
export interface TextSegmentAuthority {
|
|
49
|
+
readonly form: TextSegmentAuthorityForm;
|
|
50
|
+
/** 本层缝出来的那一段(已提交前缀 + 还押着的段缓冲)与权威全文**不逐字节相等**。 */
|
|
51
|
+
readonly diverged: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 本段在替换发生前**已经撤不回**的长度。
|
|
54
|
+
*
|
|
55
|
+
* 🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节,也不是「几条消息/几条帧」**
|
|
56
|
+
* (0.68.1 轮三 finding① / 轮二 finding② 两条订正的合并结论)。按 UTF-8 字节去截会在任何
|
|
57
|
+
* 非 ASCII 正文上截错位置;按「消息」去丢会把已经定稿的上一段一起删掉。
|
|
58
|
+
* 🔴 它按**替换前的拼文**计长,**不是** `content` 上的偏移量。
|
|
59
|
+
*/
|
|
60
|
+
readonly committedPrefixLen: number;
|
|
61
|
+
/** 已提交的那一截**自己**与权威全文对不上(= 凭据落在已经出门的那一截里)。 */
|
|
62
|
+
readonly committedPrefixDiverged: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `replace_tail` 形下,未提交部分该换成的那一截 = `content.slice(committedPrefixLen)`。
|
|
65
|
+
*
|
|
66
|
+
* 🔴 另外两形下恒为空串,**而且不许拿它当「没有尾段」的判据** —— `withhold` 形下权威全文整份
|
|
67
|
+
* 都在帧上,由宿主换掉前缀那一截;`untouched` 形下本层压根没有可替换的对象。分支看
|
|
68
|
+
* {@link form},不看这一位的长度。
|
|
69
|
+
*/
|
|
70
|
+
readonly tail: string;
|
|
71
|
+
}
|
|
72
|
+
/** {@link resolveTextSegmentAuthority} 的输入 —— 调用层把自己那两截字节交进来。 */
|
|
73
|
+
export interface TextSegmentAuthorityInput {
|
|
74
|
+
/**
|
|
75
|
+
* 本段**已经撤不回**的那一截(交互车道 = 两本前缀账相加;print 车道 = 跨工具卡累加的已发前缀)。
|
|
76
|
+
*
|
|
77
|
+
* 🔴 **必须是相加后的那一份,不是二选一**(0.68.1 轮二 finding① 实抓):openai 车道的
|
|
78
|
+
* `text_end` 在 `finalize()` 里才 push,而工具卡先执行 —— 一段散文可以跨过两张卡,每一张
|
|
79
|
+
* 都带走一截。少算任何一截,`content.startsWith(前缀)` 这个判据就会在**没分歧**的常态上
|
|
80
|
+
* 误判成分歧。
|
|
81
|
+
*/
|
|
82
|
+
readonly committedPrefix: string;
|
|
83
|
+
/** 本段**还押在调用层手里、可以改**的那一截(段缓冲 / 未封存的块尾)。 */
|
|
84
|
+
readonly openSegment: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 权威段替换的**六形判决**(两条车道共用的那一份;`content` 是引擎报的段权威全文)。
|
|
88
|
+
*
|
|
89
|
+
* 六形与它们各自的判据锚:
|
|
90
|
+
* (a) 常态:整段还押在调用层手里 ⇒ `replace_tail`,`committedPrefixLen === 0`;
|
|
91
|
+
* (b1) 半段已出门、且 `content.startsWith(前缀)` ⇒ `replace_tail`,只换未出门那一截;
|
|
92
|
+
* (b2) 半段已出门、**前缀自己也过期** ⇒ `withhold`。🔴 判据锚 = `startsWith`,**不是**「有没有
|
|
93
|
+
* flush/提交过」—— 后者是前置条件,前者才是真正决定结果的那个量;
|
|
94
|
+
* (c) 一 turn 多段(anthropic 车道逐 `content_block_stop` 发 `text_end`):本函数只交尾段,
|
|
95
|
+
* 「换完即封存、下一段另起」由调用层落账 —— 不封存的话第二帧会把第一段正文一起覆盖;
|
|
96
|
+
* (d) 子流(`parentToolCallId` 在场):**根本不该走到这里**,断闸在调用层的臂上(拿子代的段边界
|
|
97
|
+
* 去改 leader 的缓冲 = 跨 lane 状态破坏)。本函数不认识 lane 位,也刻意不去认识它;
|
|
98
|
+
* (e) 迟到的段边界(openai 车道:工具卡先执行、`text_end` 在 `finalize()` 才到)⇒ 由调用层把
|
|
99
|
+
* 两本账相加后交进 {@link TextSegmentAuthorityInput.committedPrefix},照 (b1)/(b2) 分流;
|
|
100
|
+
* (f) 调用层一个字节都没经手这一段(两截皆空)⇒ `untouched`。替换的语义是「换掉本层自己缝合
|
|
101
|
+
* 出来的那一段」,不是「凭空铸一段」:durable 整块 `text` 腿之后再来一帧 `text_end`,塞进
|
|
102
|
+
* 空缓冲就会在收口**再铸一条一模一样的**(0.68.1 `#323-g` 正控守的就是这一形)。
|
|
103
|
+
*
|
|
104
|
+
* @param content 该段的权威全文(UNTRUSTED、仅展示,与 `text_delta` 同契约 —— 渲染,绝不回喂模型)。
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveTextSegmentAuthority(content: string, input: TextSegmentAuthorityInput): TextSegmentAuthority;
|
|
107
|
+
/**
|
|
108
|
+
* print 形车道的**段账**(L-318;交互车道不吃这一只 —— 它的账按 transcript 消息分两本,见文件头注)。
|
|
109
|
+
*
|
|
110
|
+
* 归包的理由与 `buildTaskRequest` / `taskNotificationToPrintFrame` 同一条(多端改造设计稿 §8-5):
|
|
111
|
+
* 这是一台**状态机**(什么时候前缀作废、什么时候段封顶、扣下的段怎么在终局判重),属三端公共判定;
|
|
112
|
+
* 「把尾段写进哪一个 content block」才是端的装配动作。
|
|
113
|
+
*
|
|
114
|
+
* 🔴 本台账**不持有正文块**:它只记「已经出门的那一截」「被扣下的那些段」。块对象身份锚
|
|
115
|
+
* (封存点属于哪一个块)必须留在端上 —— 那是 CC 帧形的事,包里没有那个概念。
|
|
116
|
+
*/
|
|
117
|
+
export interface TextSegmentLedger {
|
|
118
|
+
/**
|
|
119
|
+
* 一截字节**从这一拍起出门了**(print 车道 = 叙述搭上 tool_use 那条 assistant 帧发走)。
|
|
120
|
+
* 🔴 **累加,不是覆盖**:一段散文可以跨过两张卡,每张带走一截(见
|
|
121
|
+
* {@link TextSegmentAuthorityInput.committedPrefix} 的 (e) 形注)。
|
|
122
|
+
*/
|
|
123
|
+
noteFlushed(text: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* 段账整只作废。三个调用点,语义同为「这一段到此为止,已发前缀对下一段无意义」:
|
|
126
|
+
* · **durable 整块 `text` 臂**:那一帧本身就是这一段的唯一载体(server 对 durable 行在写口与
|
|
127
|
+
* 读口各脱一次,它本身就是权威形),本层攒的段被它取代;
|
|
128
|
+
* · **模型轮收口**(`turn_end`):一个引擎段不可能跨模型轮(每次 API 调用有自己的 content
|
|
129
|
+
* block)⇒ 「这一段的 `text_end` 此后不会再来了」是事实,不是猜。不清账的后果实测过:
|
|
130
|
+
* 引擎省掉 `text_end` 又继续下一轮时,上一轮的已发前缀会让下一轮**正常**的 `text_end`
|
|
131
|
+
* 误入 (b2),新终答被整段清掉还附一条错的披露行;
|
|
132
|
+
* · **终局 flush**:流到此为止。
|
|
133
|
+
*/
|
|
134
|
+
closeSegmentAccount(): void;
|
|
135
|
+
/**
|
|
136
|
+
* 引擎明说这一段写完了 ⇒ 出判决 + 收账(`withhold` 形同批登记被扣段,供
|
|
137
|
+
* {@link TextSegmentLedger.repeatsWithheldSegment} 在终局判重)。
|
|
138
|
+
*
|
|
139
|
+
* @param openSegment 端此刻还押在手里、可以改的那一截(未封存的块尾)。
|
|
140
|
+
*/
|
|
141
|
+
applyTextEnd(content: string, openSegment: string): TextSegmentAuthority;
|
|
142
|
+
/**
|
|
143
|
+
* 终局兜底臂的让位判据 —— 「这一发要交的,**就是**刚才被扣的那一段吗」。
|
|
144
|
+
*
|
|
145
|
+
* 🔴 判据 = 规范化(`trim`)后**逐字相等**,不是子串关系(0.68.1 轮二两条实复现反例):
|
|
146
|
+
* ① 被扣段短到只剩一个脱敏标记时,后续**另一段**独立终答只要提到同一个标记就被整条挡掉
|
|
147
|
+
* (`includes` 误挡);② 被扣段以换行结尾时,引擎的 `assembleResult` 会 **trim** 最终正文
|
|
148
|
+
* ⇒ `includes` 不匹配,权威全文被兜底臂完整再发一次(漏挡)。
|
|
149
|
+
* 🔴 **登记的是一张表不是一个值**:一条 run 里可以有多段先后落 (b2)(多轮工具循环),
|
|
150
|
+
* 只记最后一段会让更早那段的重发漏挡。
|
|
151
|
+
*/
|
|
152
|
+
repeatsWithheldSegment(candidate: string): boolean;
|
|
153
|
+
/** 当前已出门前缀的长度(诊断/断言读位;分支判决一律走 {@link applyTextEnd} 的回执)。 */
|
|
154
|
+
readonly committedPrefixLength: number;
|
|
155
|
+
}
|
|
156
|
+
/** 建一本 print 形段账(一条流一本;跨流复用会把上一条流的已发前缀算进这一条)。 */
|
|
157
|
+
export declare function createTextSegmentLedger(): TextSegmentLedger;
|