@sema-agent/client-core 0.62.0 → 0.62.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 +73 -0
- package/README.md +1 -1
- package/dist/adapter/runStream.js +1 -0
- package/dist/subagentContentStore.d.ts +7 -1
- package/dist/subagentContentStore.js +332 -56
- package/docs/INTEGRATION-CLIENTS.md +111 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,79 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.62.2(2026-09-09)
|
|
53
|
+
|
|
54
|
+
> patch 内容批(一件)。**本段只记内容**:`package.json.version` 未动、README `Version` 行未动、
|
|
55
|
+
> `FROZEN` 未动 —— bump 与段头转日期形归**发包批**(阶段一义务见本档头注)。**零新导出、零型面变化**,
|
|
56
|
+
> 老宿主一个字节不改照常跑;`peerDependencies` 未动。
|
|
57
|
+
|
|
58
|
+
- 🔴 **子代内容账本:工具卡的阶段账与卡上的内容改成同源**(行为**修复**,异源复审第四轮四条)。
|
|
59
|
+
0.62.1 把「这张卡现在承载的是哪一阶段」记在一本**只在 `eventId` 在场时才写、且 `start`/`end`
|
|
60
|
+
两半各自比较**的水位上,而内容(名 / 参 / 结果 / 段边界)不论身份在不在都照落 —— 身份账与内容账
|
|
61
|
+
**不同源**。同一个病根开出四个用户可见的口子,本批一并修掉:
|
|
62
|
+
- **无身份帧改了内容,旧水位还替它背书**:先落新参数 + 真结果(带身份),再收一发**不带**
|
|
63
|
+
`eventId` 的旧参数 / 失败结果 ⇒ 内容按「后到落账」被覆盖,而水位仍停在覆盖**之前**那一阶段
|
|
64
|
+
⇒ 随后正确的那一发**原样重放**被自己的水位整帧拦掉,卡**永久**停在旧参数与失败结果上。
|
|
65
|
+
改为:身份缺席的那一发**若真的改了**那一半的内容,落账时同拍把对应那一半水位清掉(判不出新旧
|
|
66
|
+
就诚实地说判不出);**内容一个字节没变**(重放送来的是同一份值)⇒ 水位原样保留 —— 它仍如实描述
|
|
67
|
+
卡上这份内容,清掉等于顺手把「旧一阶段不许盖新一阶段」那道闸一起卸了。一句话:**这一半的水位
|
|
68
|
+
永远描述卡上这一半现在这份内容,内容没动它就没理由动**。
|
|
69
|
+
- **收口不看 `start` 半边**:两条独立的流交错时(恢复阶段的 `tool_start` 已到、旧一阶段的
|
|
70
|
+
`tool_end` 才姗姗来迟),旧收口会落到新阶段那张**开着**的卡上并把它关掉 ⇒ 新参数配上旧阶段的
|
|
71
|
+
失败结果;新流若中断,这个错误状态一直留在屏上。改为:收口**两半都比**,早于当前 `start` 的
|
|
72
|
+
整帧丢弃 —— 更新的 `start` 已经建立了新的一阶段,旧收口不属于它(那张卡确实还在飞,
|
|
73
|
+
显示在飞是**如实**的)。
|
|
74
|
+
- **「水位缺席」被当成「首个段边界还没给过」**:上游不给 `eventId` 时 `start` 水位永远抬不起来
|
|
75
|
+
⇒ **每一次重放都再段闭合一次**,还在流的活体尾段被切成历史 item,随后该段的聚合全文在空缓冲上
|
|
76
|
+
吸不到前缀、整只落回缓冲 ⇒ 前缀重复上屏。改为:段边界给过没有是**卡上的一位**,不依赖可选身份。
|
|
77
|
+
- **段边界只按内容串记、不按段身份记**:`AAA` → 收口先到 → `BBB` → 迟到的 `tool_start` 这种乱序里,
|
|
78
|
+
那一发 start 把 `AAABBB` 整只闭合成**一段** ⇒ 整轮重放送来 `AAA`、`BBB` 两段时两道内容闸都不命中,
|
|
79
|
+
历史与缓冲各留一份。改为:收口先到那一刻把缓冲上的位置记进**这条子代的账本**(不是记在卡上),
|
|
80
|
+
段闭合时在**所有**还没兑现的位置上一起切,并把在场每张卡的「还欠不欠一次段闭合」一并置上。
|
|
81
|
+
位置是**缓冲的**属性、不是那张卡的私产:①兑现者不限于那一发迟到的 `tool_start`(别的工具起头、
|
|
82
|
+
`settle` 同样要切);②那一发迟到的 start 若还当自己欠着,会在别人切完之后再切一刀,切掉的是
|
|
83
|
+
当前那条与它无关的活体尾段;③卡被帽丢掉不等于缓冲被丢掉(帽先丢 item、丢不够才裁缓冲),
|
|
84
|
+
边界跟着卡消失就是把「这里有过一次工具」丢了;④缓冲头部被裁时边界同拍左移。上限 64 条。
|
|
85
|
+
同批把「开着的卡 → 下标」那张表删掉(卡在不在、在哪里只由 `items` 说了算,那是同一件事的第二处
|
|
86
|
+
真源),`tool_end` 的两支从此共用同一条定位与同一道闸。接入面逐条见 `docs/INTEGRATION-CLIENTS.md`
|
|
87
|
+
§26b;门 `run-subagent-durable-divert-test.mjs` 新增 G13–G16 四块(90 ⇒ 108 checks),每块除终态外都
|
|
88
|
+
断言**中间快照**、**段条数**与**字节账**(留痕 `droppedBytes` 与门自算的真实字节逐字节相等);另含
|
|
89
|
+
代理对被拆两帧时段边界的退让格、`settle` 兑现边界格、两张收口先到的卡交错格、切点被别人兑现之后
|
|
90
|
+
迟到 start 不再切格、卡被裁而缓冲完好格、缓冲头部被裁后边界左移格。
|
|
91
|
+
|
|
92
|
+
## 0.62.1(2026-09-09)
|
|
93
|
+
|
|
94
|
+
> patch 内容批(两件)。**本段只记内容**:`package.json.version` 未动、README `Version` 行未动、
|
|
95
|
+
> `FROZEN` 未动 —— bump 与段头转日期形归**发包批**(阶段一义务见本档头注)。两件都**零新导出、
|
|
96
|
+
> 零型面变化**,老宿主一个字节不改照常跑;`peerDependencies` 未动(sdk 地板仍 `>=8.5.0`)。
|
|
97
|
+
|
|
98
|
+
- 🔴 **子代内容账本:工具两臂补上重放幂等闸**(行为**修复**)。0.62.0 只给聚合两臂
|
|
99
|
+
(`text` / `reasoning`)上了两道闸,`tool_start` / `tool_end` **一道都没有** —— 断线重连后
|
|
100
|
+
durable 腿整轮重放时,同一次工具调用在子代查看态里排出**两张卡**、结果正文被记两遍
|
|
101
|
+
(内容字节预算跟着虚高)。现形:判据形与散文两臂相同(先问身份、再问账本),身份 = `toolCallId`、
|
|
102
|
+
账本 = 查看态 `items` 上那张卡 —— **刻意不共用聚合帧那本有界身份记号**(它的额度按「聚合帧只在
|
|
103
|
+
重放腿上来」定,而工具帧活体也来,跟着写会把还在屏上的散文身份挤掉)。
|
|
104
|
+
🔴 判据不是「见过没见过」而是**阶段的先后**:同一个 `toolCallId` 真的会有**第二对** start/end ——
|
|
105
|
+
门把调用 park 掉、批复后恢复执行时引擎复用同一个 id 再发一对,第二对带的才是真结果(批复还可能
|
|
106
|
+
改过参数)。库为每张卡记一条**阶段水位**(已落账的 start / end 身份),按 `eventId` 的先后判新旧:
|
|
107
|
+
比水位旧或就是同一条帧 ⇒ 整帧丢弃;更新 ⇒ 后到的才算数、就地替换(按差额记账)。可比性的前提是
|
|
108
|
+
`eventId` **时间可排序**(上游铸的 uuidv7);缺席 ⇒ 判不出 ⇒ 退回「后到的落账」(空操作)。
|
|
109
|
+
水位与卡**同生共死**。四条细则:①`tool_start` 不铸第二张卡;这张卡**还没落过 start**(收口先到时
|
|
110
|
+
合成的占位卡)⇒ 先段闭合再补齐名参,水位已在 ⇒ 不段闭合;②旧一阶段的 start 不许把改后参数盖回;
|
|
111
|
+
③`tool_end` **先问卡还开着吗** —— 开着就落结果(在飞的卡被拦掉收口 = 永远在飞);④收口找不到卡时
|
|
112
|
+
合成一张已完成的卡、**不**在那里段闭合(边界由迟到的 start 补做;在收口处闭合会把还在流的一段
|
|
113
|
+
提前切成历史 item,紧帽下随即被丢)。去重射程严格是**同一次调用** —— 另一个 `toolCallId` 的卡照常进。
|
|
114
|
+
判据见 `docs/INTEGRATION-CLIENTS.md` §26b、§26j **G-14**;常驻门
|
|
115
|
+
`scripts/run-subagent-durable-divert-test.mjs` 新增 G8 / G9 / G10 / G11 四段。
|
|
116
|
+
- 📋 **§26j 黑盒判据 G-12 改写**(判据面订正,**产品字节零变化**)。原文「诊断行显示长度**不超过**
|
|
117
|
+
同长度良性值那一行」超出实现的承诺:呈前封长是**先转义、后按转义结果封长**,一个不可见字符转义后
|
|
118
|
+
占 6 个显示字符 —— 转义后仍在帽内时一枚都不用截,那一行必然**长于**同长度的良性行(良性行 1:1)。
|
|
119
|
+
改写后的 G-12 判的是真正承诺的量:**≤ 帽、行尾无半截转义序列、转义后仍在帽内的值不截**,且判据量
|
|
120
|
+
一律是**转义后的显示长度**(原文短于帽但转义后超帽 ⇒ 照样截)。常驻门
|
|
121
|
+
`scripts/run-display-cap-order-test.mjs` 同批补三档(转义后在帽内不截且逐序列完整 / 原文短而转义
|
|
122
|
+
超帽照截在序列边界 / 恰好抵帽时行尾不是半截)。
|
|
123
|
+
- 子代内容账本:迟到的 `tool_start` 触发条数裁剪后不再按裁剪前的下标/引用改名补参、写差额(异源复审第四轮 [high]);flush 之后按 `toolCallId` 重新找卡,卡已被裁掉 ⇒ 按「新的一次」重建(§26b 留白①);门 G12 三格。
|
|
124
|
+
|
|
52
125
|
## 0.62.0(2026-09-09)
|
|
53
126
|
|
|
54
127
|
> 内容批(隔离树交付,七件)。**本段只记内容**(0.60.0/0.61.0 先例逐字):`package.json.version`
|
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.62.
|
|
38
|
+
**Version:** 0.62.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
|
|
@@ -331,6 +331,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
331
331
|
text: sub.text,
|
|
332
332
|
// 聚合帧的**幂等键**:durable 重放会把整轮的段再送一遍,而缝前的事件序号去重只在**同一条**
|
|
333
333
|
// 流内有效(重连后是新的一条流)。把身份交给 store,让跨流的重放也只收一次。
|
|
334
|
+
// (工具两臂不读这一位 —— store 侧按 `toolCallId` 与账本上那张卡判,见 subagentContentStore。)
|
|
334
335
|
eventId: sub.eventId,
|
|
335
336
|
toolCallId: sub.toolCallId,
|
|
336
337
|
toolName: sub.toolName,
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* —— 🔴 **durable 腿(重放)送的是同一段的聚合形** `text` / `reasoning`(整段全文,同样带
|
|
14
14
|
* EventIdentity)。两类都要收:只收增量的话,续听重放尾的宿主整条子代转录都是空的
|
|
15
15
|
* (0.62.0 补;两类的处置**不同**,见 {@link publishSubagentContentEvent} 的吸收规则)
|
|
16
|
+
* 🔴 **重放的幂等闸覆盖四个臂**(`text` / `reasoning` / `tool_start` / `tool_end`):重连 = 新的一条
|
|
17
|
+
* 流,缝前那道事件序号去重只在同一条流内有效,所以「整轮重放」拦得住的只能是这本账。判据形相同
|
|
18
|
+
* (先问身份、再问账本),身份与账本各按臂取:散文两臂 = 聚合帧 `eventId` + 已上屏的整段正文;
|
|
19
|
+
* 工具两臂 = `toolCallId` + `items` 上那张卡,收口另带一本**自己额度**的收口身份账(同一次调用
|
|
20
|
+
* 会有**第二发**真收口 —— 门 park 之后恢复执行,见 {@link publishSubagentContentEvent})。
|
|
16
21
|
* (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
|
|
17
22
|
* (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
|
|
18
23
|
* `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
|
|
@@ -91,7 +96,8 @@ export interface SubagentContentEvent {
|
|
|
91
96
|
/** 聚合两臂(`text` / `reasoning`)的**整段全文**。刻意不复用 `delta` 位:同名不同义会让
|
|
92
97
|
* 「这一位是增量还是全文」变成读者要靠 `type` 去反推的事,而反推错的代价是内容重复上屏。 */
|
|
93
98
|
text?: string | undefined;
|
|
94
|
-
/** wire 上那一帧的**事件身份**(聚合两臂的幂等键;缺席 ⇒ 退回内容判据,见 publish 的两道闸)。
|
|
99
|
+
/** wire 上那一帧的**事件身份**(聚合两臂的幂等键;缺席 ⇒ 退回内容判据,见 publish 的两道闸)。
|
|
100
|
+
* 🔴 工具两臂**不读这一位**:那次调用的身份是 `toolCallId`,账本是 `items` 上那张卡。 */
|
|
95
101
|
eventId?: string | undefined;
|
|
96
102
|
toolCallId?: string | undefined;
|
|
97
103
|
toolName?: string | undefined;
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* —— 🔴 **durable 腿(重放)送的是同一段的聚合形** `text` / `reasoning`(整段全文,同样带
|
|
14
14
|
* EventIdentity)。两类都要收:只收增量的话,续听重放尾的宿主整条子代转录都是空的
|
|
15
15
|
* (0.62.0 补;两类的处置**不同**,见 {@link publishSubagentContentEvent} 的吸收规则)
|
|
16
|
+
* 🔴 **重放的幂等闸覆盖四个臂**(`text` / `reasoning` / `tool_start` / `tool_end`):重连 = 新的一条
|
|
17
|
+
* 流,缝前那道事件序号去重只在同一条流内有效,所以「整轮重放」拦得住的只能是这本账。判据形相同
|
|
18
|
+
* (先问身份、再问账本),身份与账本各按臂取:散文两臂 = 聚合帧 `eventId` + 已上屏的整段正文;
|
|
19
|
+
* 工具两臂 = `toolCallId` + `items` 上那张卡,收口另带一本**自己额度**的收口身份账(同一次调用
|
|
20
|
+
* 会有**第二发**真收口 —— 门 park 之后恢复执行,见 {@link publishSubagentContentEvent})。
|
|
16
21
|
* (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
|
|
17
22
|
* (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
|
|
18
23
|
* `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
|
|
@@ -191,6 +196,22 @@ function appendBytes(prev, chunk) {
|
|
|
191
196
|
const joinsPair = lastPrev >= 0xd800 && lastPrev <= 0xdbff && firstNew >= 0xdc00 && firstNew <= 0xdfff;
|
|
192
197
|
return joinsPair ? add - 2 : add;
|
|
193
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* 工具参数(开集 `unknown`)的**规范序列化** —— 字节账({@link itemBytesOf})与「这一发到底改了
|
|
201
|
+
* 没有」({@link startContentKey})共用这**一口**,两处对同一份参数的看法因此不可能分叉。
|
|
202
|
+
* 不可序列化(循环引用等)⇒ 空串:它进不了展示面,也就占不了这本账的预算、也不算内容变化。
|
|
203
|
+
*/
|
|
204
|
+
function serializeToolInput(input) {
|
|
205
|
+
if (input === undefined)
|
|
206
|
+
return '';
|
|
207
|
+
try {
|
|
208
|
+
return JSON.stringify(input) ?? '';
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// 循环引用/不可序列化 ⇒ 这一位按空串计(见本函数头注)
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
194
215
|
/** 一条 item 的字节账(`truncated` 是关于预算的元信息,自身不计)。 */
|
|
195
216
|
function itemBytesOf(it) {
|
|
196
217
|
switch (it.kind) {
|
|
@@ -198,18 +219,9 @@ function itemBytesOf(it) {
|
|
|
198
219
|
case 'thinking':
|
|
199
220
|
case 'echo':
|
|
200
221
|
return utf8Len(it.text);
|
|
201
|
-
case 'tool':
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
if (it.input !== undefined)
|
|
206
|
-
n += utf8Len(JSON.stringify(it.input) ?? '');
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
// 循环引用/不可序列化 ⇒ 这一位按 0 计(它进不了展示面,也就占不了这本账的预算)
|
|
210
|
-
}
|
|
211
|
-
return n;
|
|
212
|
-
}
|
|
222
|
+
case 'tool':
|
|
223
|
+
// 工具参数序列化一次算体量(每张卡只算一次,不在增量热路径上)。
|
|
224
|
+
return utf8Len(it.name) + utf8Len(it.output ?? '') + utf8Len(serializeToolInput(it.input));
|
|
213
225
|
case 'truncated':
|
|
214
226
|
return 0;
|
|
215
227
|
}
|
|
@@ -251,15 +263,38 @@ function noteDropped(s, dropped) {
|
|
|
251
263
|
}
|
|
252
264
|
s.items.unshift({ kind: 'truncated', droppedBytes: s.droppedBytes, keptFrom: s.droppedBytes });
|
|
253
265
|
s.itemBytes.unshift(0);
|
|
254
|
-
|
|
266
|
+
pruneCardPhases(s);
|
|
255
267
|
}
|
|
256
|
-
/**
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
/**
|
|
269
|
+
* items 被增删之后,把**卡已不在 items 上**的阶段账清掉。
|
|
270
|
+
*
|
|
271
|
+
* 🔴 这里此前还维护第二张表(`toolCallId` → 开着的卡下标)。删掉它是本批「按类修」的一部分:
|
|
272
|
+
* 一次工具调用的卡**在不在、在哪里**只有 `items` 说了算(本文件 {@link toolCardIndexOf} 头注
|
|
273
|
+
* 逐字写过这条),而下标表是同一件事的第二处真源 —— 它与 items 脱节的方式是可预期的
|
|
274
|
+
* (splice/unshift 之后下标整体漂移),而本批四条的病根正是「同一件事记了两本各自记的账」。
|
|
275
|
+
* 收口那一支改成每次按 `toolCallId` 现查(至多 200 条,一次线性扫),两处真源就只剩一处。
|
|
276
|
+
*/
|
|
277
|
+
function pruneCardPhases(s) {
|
|
278
|
+
// 🔴 只清**卡的**阶段账。段边界不在这里 —— 它压在缓冲上(TaskContentState.pendingCuts),卡被丢掉
|
|
279
|
+
// 时缓冲里那截正文一个字都没少,边界跟着卡消失就等于把「这里有过一次工具」这件事丢了。
|
|
280
|
+
const live = new Set();
|
|
281
|
+
for (const it of s.items) {
|
|
282
|
+
if (it.kind === 'tool')
|
|
283
|
+
live.add(it.id);
|
|
284
|
+
}
|
|
285
|
+
for (const id of s.toolPhase.keys()) {
|
|
286
|
+
if (!live.has(id))
|
|
287
|
+
s.toolPhase.delete(id);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** 取(必要时新建)一张卡的阶段账 —— 卡与账同生:建卡/改卡的每一支都经这一口。 */
|
|
291
|
+
function phaseSlot(s, id) {
|
|
292
|
+
const cur = s.toolPhase.get(id);
|
|
293
|
+
if (cur !== undefined)
|
|
294
|
+
return cur;
|
|
295
|
+
const next = { boundary: false };
|
|
296
|
+
s.toolPhase.set(id, next);
|
|
297
|
+
return next;
|
|
263
298
|
}
|
|
264
299
|
/**
|
|
265
300
|
* 每子代帽:超出就**从最早的内容开始丢**,丢不动了再从缓冲头部裁。
|
|
@@ -273,6 +308,13 @@ function enforceTaskCap(s) {
|
|
|
273
308
|
if (s.bytes <= cap)
|
|
274
309
|
return false;
|
|
275
310
|
let dropped = 0;
|
|
311
|
+
// 「有没有卡离开 items」按**条数**量,不拿 `dropped`(字节)代答。
|
|
312
|
+
// 📋 如实(变异自证):这一改**今天零行为差异** —— 每子代帽恒 ≥ 1(configureSubagentContentStore
|
|
313
|
+
// 的 fail-loud 挡住 0 与负数),而循环只在「还装不下」时继续,所以有卡离开就一定有字节离开,
|
|
314
|
+
// 两个条件当下等价;门对它没有判别力是**正常**的,别拿门当它的证明。这样写的理由是:
|
|
315
|
+
// 「卡走了、它的阶段账同批清」是一条**行为面**不变量(陈旧的 `end` 会把重建那一轮的收口整帧
|
|
316
|
+
// 拦下 —— 卡建回来却永远显示在飞,G9 那一格量的就是它),它不该靠一条关于**字节**的推论维持。
|
|
317
|
+
let removedItems = 0;
|
|
276
318
|
const first = () => (s.items[0]?.kind === 'truncated' ? 1 : 0);
|
|
277
319
|
while (s.bytes > cap && s.items.length > first()) {
|
|
278
320
|
const idx = first();
|
|
@@ -281,25 +323,32 @@ function enforceTaskCap(s) {
|
|
|
281
323
|
s.itemBytes.splice(idx, 1);
|
|
282
324
|
addBytes(s, -b);
|
|
283
325
|
dropped += b;
|
|
326
|
+
removedItems += 1;
|
|
284
327
|
}
|
|
328
|
+
// 🔴 裁的是缓冲**头部** ⇒ 压在这条缓冲上的段边界同拍左移(边界是位置,前缀短了它就得跟着短;
|
|
329
|
+
// 不移的话那一刀会切在错的字上)。段边界活得比卡长,所以这一步是真会走到的:上面那个循环
|
|
330
|
+
// 可以只丢掉卡而缓冲一个字没少,反过来这里裁了缓冲而边界还压着。
|
|
285
331
|
if (s.bytes > cap) {
|
|
286
332
|
const cut = trimHeadBytes(s.thinkBuf, s.bytes - cap);
|
|
333
|
+
shiftPendingCuts(s, 'think', s.thinkBuf.length - cut.rest.length);
|
|
287
334
|
s.thinkBuf = cut.rest;
|
|
288
335
|
addBytes(s, -cut.removed);
|
|
289
336
|
dropped += cut.removed;
|
|
290
337
|
}
|
|
291
338
|
if (s.bytes > cap) {
|
|
292
339
|
const cut = trimHeadBytes(s.textBuf, s.bytes - cap);
|
|
340
|
+
shiftPendingCuts(s, 'text', s.textBuf.length - cut.rest.length);
|
|
293
341
|
s.textBuf = cut.rest;
|
|
294
342
|
addBytes(s, -cut.removed);
|
|
295
343
|
dropped += cut.removed;
|
|
296
344
|
}
|
|
345
|
+
if (removedItems > 0)
|
|
346
|
+
pruneCardPhases(s);
|
|
297
347
|
if (dropped > 0) {
|
|
298
|
-
rebuildOpenTools(s);
|
|
299
348
|
noteDropped(s, dropped);
|
|
300
349
|
return true;
|
|
301
350
|
}
|
|
302
|
-
return
|
|
351
|
+
return removedItems > 0;
|
|
303
352
|
}
|
|
304
353
|
/**
|
|
305
354
|
* 总帽:整条**最久未用**的子代账本被清掉(`tasks` 的迭代序就是 LRU 序,活跃项每次访问都会重插)。
|
|
@@ -359,12 +408,13 @@ function stateFor(taskId, parentToolCallId) {
|
|
|
359
408
|
itemBytes: [],
|
|
360
409
|
textBuf: '',
|
|
361
410
|
thinkBuf: '',
|
|
362
|
-
openTools: new Map(),
|
|
363
411
|
parentToolCallId,
|
|
364
412
|
bytes: 0,
|
|
365
413
|
droppedBytes: 0,
|
|
366
414
|
seenAggregateIds: new Set(),
|
|
367
415
|
recordedSegments: new Set(),
|
|
416
|
+
pendingCuts: [],
|
|
417
|
+
toolPhase: new Map(),
|
|
368
418
|
};
|
|
369
419
|
tasks.set(taskId, s);
|
|
370
420
|
}
|
|
@@ -388,29 +438,81 @@ function pushItem(s, item) {
|
|
|
388
438
|
addBytes(s, -freed);
|
|
389
439
|
// 条数帽丢掉的同样是**内容**,同样要留痕(此前这条路是无声的)。
|
|
390
440
|
noteDropped(s, freed);
|
|
391
|
-
//
|
|
392
|
-
|
|
441
|
+
// 卡被裁掉了 ⇒ 它的阶段账同批作废(卡没了,那本账就没有指涉对象)
|
|
442
|
+
pruneCardPhases(s);
|
|
393
443
|
}
|
|
394
444
|
}
|
|
395
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* 段闭合:把两条缓冲收成 item(段边界 = 工具起头,或这一轮 settle)。
|
|
447
|
+
*
|
|
448
|
+
* 🔴 **在所有还没兑现的段边界处一起切**({@link CardPhase.cut};收口先于开始帧到达时记下的位置)。
|
|
449
|
+
* 为什么不是「只切触发这一次闭合的那张卡的那一条」:边界记的是**缓冲上的位置**,不是某张卡的
|
|
450
|
+
* 私产 —— 一条缓冲上可以同时压着好几条(两张 orphan 卡交错),而闭合一旦发生缓冲就整只清空,
|
|
451
|
+
* 没被兑现的那些位置从此无处可切。异源复审 R5 的两个反例都是这一形:①兑现者是 `settle` 或
|
|
452
|
+
* 别的工具的 `tool_start`(它们此前根本不看这些位置)⇒ 两段黏成一段;②两张 orphan 交错时第一发
|
|
453
|
+
* 迟到的 start 只兑现自己那条、把另一条丢掉 ⇒ 后两段黏成一段。黏成一段的代价是整轮重放时那一段
|
|
454
|
+
* 的内容闸不命中,历史与缓冲各留一份。
|
|
455
|
+
*/
|
|
396
456
|
function flushBuffers(s) {
|
|
457
|
+
const asc = (a, b) => a - b;
|
|
458
|
+
const textCuts = [...new Set(s.pendingCuts.map((c) => c.text))].sort(asc);
|
|
459
|
+
const thinkCuts = [...new Set(s.pendingCuts.map((c) => c.think))].sort(asc);
|
|
397
460
|
// 🔴 缓冲的字节此前已经计过账;搬进 item 时先扣掉缓冲那一份,再由 pushItem 计 item 那一份
|
|
398
461
|
// (纯空白段被丢弃时同样要扣 —— 少扣一次,总账就会一路虚高到把别人的账本淘汰掉)。
|
|
399
462
|
addBytes(s, -utf8Len(s.thinkBuf));
|
|
400
463
|
const think = s.thinkBuf;
|
|
401
464
|
s.thinkBuf = '';
|
|
402
|
-
|
|
403
|
-
pushItem(s, { kind: 'thinking', text: think });
|
|
404
|
-
rememberSegment(s, think);
|
|
405
|
-
}
|
|
465
|
+
closeSegments(s, 'thinking', think, thinkCuts);
|
|
406
466
|
addBytes(s, -utf8Len(s.textBuf));
|
|
407
467
|
const text = s.textBuf;
|
|
408
468
|
s.textBuf = '';
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
469
|
+
closeSegments(s, 'text', text, textCuts);
|
|
470
|
+
// 缓冲已空 ⇒ 压在它上面的位置一条不剩地兑现完了(指涉对象没有了)
|
|
471
|
+
s.pendingCuts.length = 0;
|
|
472
|
+
// 🔴 段闭合发生过了 ⇒ 在场每张卡都不再欠一次段闭合。这一位问的是「自这张卡出现以来发生过段闭合
|
|
473
|
+
// 没有」而不是「它自己切过没有」:兑现的可以是别人,而那一发迟到的 `tool_start` 若还当自己欠着,
|
|
474
|
+
// 就会在别人切完之后**再切一刀** —— 切掉的是当前那条与它无关的活体尾段。
|
|
475
|
+
for (const p of s.toolPhase.values())
|
|
476
|
+
p.boundary = true;
|
|
477
|
+
}
|
|
478
|
+
/** 一条缓冲收成 item:在每一条**落在中间**的边界上切一刀(空白段照旧丢弃、不铸空 item)。 */
|
|
479
|
+
function closeSegments(s, kind, buf, cuts) {
|
|
480
|
+
const push = (part) => {
|
|
481
|
+
if (!part.trim())
|
|
482
|
+
return;
|
|
483
|
+
pushItem(s, kind === 'text' ? { kind: 'text', text: part } : { kind: 'thinking', text: part });
|
|
484
|
+
rememberSegment(s, part);
|
|
485
|
+
};
|
|
486
|
+
let from = 0;
|
|
487
|
+
for (const at of cuts) {
|
|
488
|
+
const n = safeCut(buf, at);
|
|
489
|
+
if (n <= from || n >= buf.length)
|
|
490
|
+
continue;
|
|
491
|
+
push(buf.slice(from, n));
|
|
492
|
+
from = n;
|
|
412
493
|
}
|
|
494
|
+
push(buf.slice(from));
|
|
413
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* 把段边界夹进 `[0, buf.length]`,并且**绝不劈开合法代理对**。
|
|
498
|
+
*
|
|
499
|
+
* 🔴 为什么会劈到:边界是「记下它那一刻的缓冲长度」,而上游真的会把一对代理对拆成**两帧**送
|
|
500
|
+
* (本文件 {@link appendBytes} 头注就是为这一形写的)—— 记边界那一刻缓冲尾部可能正停在高位
|
|
501
|
+
* 代理项上。从那里切会切出两个孤代理项:屏上两段各挂半个字符,两段的字节账合计也比整条多 2。
|
|
502
|
+
* ⇒ 撞上就把边界**退**到那一对之前(宁可把这半个字符归给后一段)。
|
|
503
|
+
*/
|
|
504
|
+
function safeCut(buf, at) {
|
|
505
|
+
let n = at < 0 ? 0 : at > buf.length ? buf.length : at;
|
|
506
|
+
if (n > 0 && n < buf.length) {
|
|
507
|
+
const prev = buf.charCodeAt(n - 1);
|
|
508
|
+
const here = buf.charCodeAt(n);
|
|
509
|
+
if (prev >= 0xd800 && prev <= 0xdbff && here >= 0xdc00 && here <= 0xdfff)
|
|
510
|
+
n -= 1;
|
|
511
|
+
}
|
|
512
|
+
return n;
|
|
513
|
+
}
|
|
514
|
+
/** 两次段闭合之间最多记多少条段边界(见 {@link TaskContentState.pendingCuts} 的有界那一笔)。 */
|
|
515
|
+
const MAX_PENDING_CUTS = 64;
|
|
414
516
|
/** 有界 FIFO 记号(两本小账共用;只用来做幂等判定,不参与预算)。 */
|
|
415
517
|
const MAX_REPLAY_KEYS = 64;
|
|
416
518
|
function remember(set, key) {
|
|
@@ -497,7 +599,10 @@ export function registerSubagentAlias(parentToolCallId, taskId) {
|
|
|
497
599
|
/**
|
|
498
600
|
* 这一条**聚合帧**是不是「已经收下过的那一条」(durable 重放的两道闸;命中 ⇒ 整帧丢弃)。
|
|
499
601
|
*
|
|
500
|
-
*
|
|
602
|
+
* 🔴 工具两臂**同一条判据形、不同的身份**:它们不进这一口(见 publish 的工具两臂),因为
|
|
603
|
+
* `toolCallId` 就是那次调用的身份、账本就是 `items` 自己 —— 比一本有界 FIFO 强,也不占它的额度。
|
|
604
|
+
*
|
|
605
|
+
* 为什么光靠缓冲前缀吸收不够(异源复审逼出的真病):吸收规则只认**当前缓冲**那一段。一条
|
|
501
606
|
* 跨过工具边界的 turn,早先那些段早就段闭合成 item、缓冲已空 —— 重放时它们的聚合帧再来一遍,
|
|
502
607
|
* `''` 是任何串的前缀,于是整段被当成新内容塞回缓冲,而且塞在**后面那段**之后(顺序也是错的)。
|
|
503
608
|
*
|
|
@@ -521,6 +626,99 @@ function replaySeen(s, ev) {
|
|
|
521
626
|
return true;
|
|
522
627
|
return false;
|
|
523
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* 这一发工具帧比卡上现在承载的那一阶段**旧或一样**吗(旧 ⇒ 整帧丢弃)。
|
|
631
|
+
*
|
|
632
|
+
* 判据是 `eventId` 的字典序 —— 前提是它**时间可排序**(core 铸的 uuidv7,sdk `EventIdentity` 逐字)。
|
|
633
|
+
* 两侧任一缺席 ⇒ 判不出新旧,答 `false`(退回「后到的落账」:重放送的是同一份值,落账是空操作)。
|
|
634
|
+
* 逐字相等 ⇒ 同一条帧又来一遍 ⇒ 同样答 `true`(整帧丢弃,一次也不重复落账)。
|
|
635
|
+
*/
|
|
636
|
+
function notNewer(eventId, mark) {
|
|
637
|
+
if (typeof eventId !== 'string' || eventId.length === 0)
|
|
638
|
+
return false;
|
|
639
|
+
if (typeof mark !== 'string' || mark.length === 0)
|
|
640
|
+
return false;
|
|
641
|
+
return eventId <= mark;
|
|
642
|
+
}
|
|
643
|
+
/** 读一张卡的阶段账(没有 ⇒ 这张卡还没落过任何一半的帧)。 */
|
|
644
|
+
function phaseOf(s, id) {
|
|
645
|
+
return id === undefined ? undefined : s.toolPhase.get(id);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* 内容落账之后给这张卡盖章:**水位与卡上现在这份内容同源**。三形:
|
|
649
|
+
* · 身份在场 ⇒ 抬到这一发;
|
|
650
|
+
* · 身份缺席**且这一发真的改了卡上那一半的内容** ⇒ 把这一半**清掉**。此前缺席时一律「不抬」,
|
|
651
|
+
* 于是无身份的那一发把内容换掉了、水位还停在**换掉之前**那一阶段上 —— 随后正确的那一发原样
|
|
652
|
+
* 重放被自己的水位 `notNewer` 整帧拦掉,卡**永久**停在旧内容上;
|
|
653
|
+
* · 身份缺席**而内容一个字节没变**(重放送来的是同一份值)⇒ **原样保留**。水位仍然如实描述卡上
|
|
654
|
+
* 这份内容,清掉它等于顺手把「旧一阶段不许盖新一阶段」那道闸一起卸了 —— 异源复审 R5 的反例:
|
|
655
|
+
* 一发无身份的**同值**重放之后,旧一阶段的 start 就能把改后参数盖回原参,而更新的结果还留在
|
|
656
|
+
* 卡上,屏上成了「这个参数产生了那个结果」。喂点真的会混来源(tail 腿的工具帧不带 `eventId`)。
|
|
657
|
+
*
|
|
658
|
+
* 一句话:**这一半的水位,永远描述卡上这一半现在这份内容**——内容没动它就没理由动。
|
|
659
|
+
*/
|
|
660
|
+
function stampPhase(s, id, half, eventId, changed) {
|
|
661
|
+
if (id === undefined)
|
|
662
|
+
return;
|
|
663
|
+
const p = phaseSlot(s, id);
|
|
664
|
+
if (typeof eventId === 'string' && eventId.length > 0)
|
|
665
|
+
p[half] = eventId;
|
|
666
|
+
else if (changed)
|
|
667
|
+
delete p[half];
|
|
668
|
+
}
|
|
669
|
+
/** 卡上「开始那一半」的内容指纹(名 + 参数的规范序列化)—— 只用来回答「这一发真的改了吗」。 */
|
|
670
|
+
function startContentKey(it) {
|
|
671
|
+
return `${it.name}\u0000${serializeToolInput(it.input)}`;
|
|
672
|
+
}
|
|
673
|
+
/** 这张卡不欠段闭合了(建卡那一支刚刚无条件闭合过一次)。 */
|
|
674
|
+
function markBoundary(s, id) {
|
|
675
|
+
phaseSlot(s, id).boundary = true;
|
|
676
|
+
}
|
|
677
|
+
/** 收口先到 ⇒ 把**此刻**两条缓冲的长度记成这条缓冲上的一个段边界(见 TaskContentState.pendingCuts)。 */
|
|
678
|
+
function recordCut(s) {
|
|
679
|
+
if (s.pendingCuts.length >= MAX_PENDING_CUTS)
|
|
680
|
+
return;
|
|
681
|
+
s.pendingCuts.push({ text: s.textBuf.length, think: s.thinkBuf.length });
|
|
682
|
+
}
|
|
683
|
+
/** 缓冲头部被裁掉 `removed` 个 UTF-16 单元 ⇒ 压在这条缓冲上的段边界一律左移(夹到 0)。 */
|
|
684
|
+
function shiftPendingCuts(s, half, removed) {
|
|
685
|
+
if (removed <= 0)
|
|
686
|
+
return;
|
|
687
|
+
for (const c of s.pendingCuts) {
|
|
688
|
+
const next = c[half] - removed;
|
|
689
|
+
c[half] = next > 0 ? next : 0;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* 这一发**收口**比卡上现在承载的那一阶段旧吗(旧 ⇒ 整帧丢弃)。
|
|
694
|
+
*
|
|
695
|
+
* 🔴 **两半都要问**,这是 0.62.2 修的第二条:只问 `end` 的话,两条独立的流交错时(恢复阶段的
|
|
696
|
+
* `start` 已经到、旧一阶段的 `end` 才姗姗来迟)旧收口会落到新阶段那张开着的卡上 —— 新参数
|
|
697
|
+
* 配上旧阶段的失败结果,新流若中断这个错误状态就一直留在屏上。更新的 `start` 已经**建立了
|
|
698
|
+
* 新的一阶段**,旧收口不属于它;「避免永远在飞」不构成接受一份已知过期结果的理由(那张卡
|
|
699
|
+
* 确实还在飞,显示在飞是**如实**的)。
|
|
700
|
+
*/
|
|
701
|
+
function staleEnd(phase, eventId) {
|
|
702
|
+
return notNewer(eventId, phase?.start) || notNewer(eventId, phase?.end);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* 这条 `toolCallId` 在本账本里已经有卡了吗(有 ⇒ 返回下标,没有 ⇒ `-1`)。
|
|
706
|
+
*
|
|
707
|
+
* 🔴 为什么账本就是 `items` 自己、不另铸第三本小账:一次工具调用的卡**在不在屏上**只有 `items`
|
|
708
|
+
* 说了算。另开一本记号账就会有两处真源,而它们脱节的方式是可预期的 —— 条数帽/字节帽把卡丢掉
|
|
709
|
+
* 之后记号还在,重放于是被永久挡住,屏上那一格从此空着且无人知道。这里逐条问 `items`,
|
|
710
|
+
* 答案与屏上看到的恒等(上限 200 条,每帧一次线性扫,量级可忽略)。
|
|
711
|
+
* 🔴 从尾往前找:同一 id 的卡至多一张(本闸的作用就是维持这条),从尾找只是让常见形(刚落的那张)
|
|
712
|
+
* 第一下命中。
|
|
713
|
+
*/
|
|
714
|
+
function toolCardIndexOf(s, id) {
|
|
715
|
+
for (let i = s.items.length - 1; i >= 0; i--) {
|
|
716
|
+
const it = s.items[i];
|
|
717
|
+
if (it?.kind === 'tool' && it.id === id)
|
|
718
|
+
return i;
|
|
719
|
+
}
|
|
720
|
+
return -1;
|
|
721
|
+
}
|
|
524
722
|
/**
|
|
525
723
|
* 聚合帧对当前缓冲的**吸收判定**(两个聚合臂共用一份;`null` = 这一帧整只丢弃)。
|
|
526
724
|
*
|
|
@@ -592,38 +790,116 @@ export function publishSubagentContentEvent(ev) {
|
|
|
592
790
|
}
|
|
593
791
|
}
|
|
594
792
|
break;
|
|
793
|
+
// ── 工具两臂(活体与 durable 重放送的是同一形)────────────────────────────────────────
|
|
794
|
+
// 🔴 与聚合两臂**同一条判据形**(先问身份、再问账本),但身份与账本都换成工具自己的那一套:
|
|
795
|
+
// 身份 = `toolCallId`(wire 给的那次调用的 id),账本 = `items` 自己(见 toolCardIndexOf)。
|
|
796
|
+
// 刻意**不写** `seenAggregateIds`:那本记号账是有界 FIFO,额度按「聚合帧只在重放腿上来」定的
|
|
797
|
+
// (活体走 `*_delta`,一个字都不写);工具帧**活体也来**,跟着写会把还在屏上的散文身份整窗
|
|
798
|
+
// 挤掉 —— 修一个臂的重放去重、弄坏另一个臂的,不是修。
|
|
799
|
+
// 🔴 收口那一支**不按「卡开着没开着」分岔**(0.62.2 合并):卡在不在只按 `toolCallId` 现查
|
|
800
|
+
// `items`,在账就过同一道闸、走同一条落账。此前开着那一支另有一张下标表、且无条件落结果
|
|
801
|
+
// (理由写的是「在飞的卡被拦掉收口就永远在飞」)—— 那条理由在**旧一阶段的收口迟到**这一形上
|
|
802
|
+
// 是错的:更新的 `start` 已经建立了新的一阶段,那张卡确实还在跑,显示在飞是**如实**的,而
|
|
803
|
+
// 把旧阶段的失败结果配到新参数上并关掉卡才是用户看得见的损坏。
|
|
595
804
|
case 'tool_start': {
|
|
805
|
+
const startId = ev.toolCallId;
|
|
806
|
+
const seen = startId !== undefined && toolCardIndexOf(s, startId) >= 0;
|
|
807
|
+
const known = phaseOf(s, startId);
|
|
808
|
+
if (seen && notNewer(ev.eventId, known?.start))
|
|
809
|
+
break;
|
|
810
|
+
// 段闭合可能触发条数裁剪(pushItem 从头 splice + unshift 留痕)⇒ 裁剪**前**取的下标与引用一律
|
|
811
|
+
// 作废:flush 之后按 toolCallId **重新**找卡。找不到 = 这张卡刚被裁掉 ⇒ 落到下面「新的一次」
|
|
812
|
+
// 建卡那一支(§26b 留白①),绝不对着已删的引用改名补参、也绝不把差额记到顶上那条留痕上。
|
|
813
|
+
// 这张卡还欠一次段闭合 ⇒ 现在补做(段闭合会在**所有**还没兑现的段边界上一起切,并把在场
|
|
814
|
+
// 每张卡的这一位置上 —— 兑现者不限于「欠着的那一张」)。
|
|
815
|
+
if (seen && known?.boundary !== true)
|
|
816
|
+
flushBuffers(s);
|
|
817
|
+
const at = startId !== undefined ? toolCardIndexOf(s, startId) : -1;
|
|
818
|
+
if (at >= 0) {
|
|
819
|
+
// 这次调用的卡已在账 ⇒ **不铸第二张**。剩下的两件按这张卡的**阶段账**判:
|
|
820
|
+
// 🔴 ① 这一发比卡上现在承载的那一阶段旧(或就是同一条帧又来一遍)⇒ 整帧丢弃。少了这一条,
|
|
821
|
+
// 门 park 那一阶段的 start 重放会把批复时**改过的执行参数**盖回原参数 —— 屏上就成了
|
|
822
|
+
// 「这个参数产生了那个结果」,而那个结果根本不是它跑出来的。
|
|
823
|
+
// 🔴 ② 这张卡的**首个段边界**给过没有(收口先到时下面那支会合成一张占位卡:名字 `Tool`、
|
|
824
|
+
// 参数 `{}`、边界还没给)—— 没给过 ⇒ 迟到的这一发就是它的第一个段边界,要补做一次段闭合,
|
|
825
|
+
// 并且按合成那一刻记下的边界把「工具之前 / 之后」两段**分开**收(不分开的话两段黏成
|
|
826
|
+
// 一段、只进一条记号,随后整轮重放时两段的内容闸都不命中)。给过了 ⇒ 这一发是重放或
|
|
827
|
+
// 下一阶段,**不**段闭合:边界在活体那一轮已经给过,在这里切会把还在流的那一段提前截断。
|
|
828
|
+
// 🔴 判据是卡上那一位,**不是**「start 水位在不在」:水位是可选位,上游不给 `eventId` 时
|
|
829
|
+
// 它永远抬不起来 ⇒ 每一次重放都会再切一刀(0.62.2 修的第三条)。
|
|
830
|
+
// 名字与参数就地补齐/更新(重放送来的是同一份值,补齐是空操作;字节按**差额**记账)。
|
|
831
|
+
const it = s.items[at];
|
|
832
|
+
const before = itemBytesOf(it);
|
|
833
|
+
const keyBefore = startContentKey(it);
|
|
834
|
+
if (ev.toolName !== undefined)
|
|
835
|
+
it.name = ev.toolName;
|
|
836
|
+
if (ev.args !== undefined)
|
|
837
|
+
it.input = ev.args;
|
|
838
|
+
const delta = itemBytesOf(it) - before;
|
|
839
|
+
s.itemBytes[at] = (s.itemBytes[at] ?? 0) + delta;
|
|
840
|
+
addBytes(s, delta);
|
|
841
|
+
stampPhase(s, it.id, 'start', ev.eventId, startContentKey(it) !== keyBefore);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
596
844
|
flushBuffers(s);
|
|
597
|
-
const id =
|
|
845
|
+
const id = startId ?? `${ev.taskId}-tool-${s.items.length}`;
|
|
598
846
|
pushItem(s, { kind: 'tool', id, name: ev.toolName ?? 'Tool', input: ev.args ?? {} });
|
|
599
|
-
|
|
847
|
+
// 建卡这一支刚刚无条件段闭合过 ⇒ 边界就是此刻,同拍记在卡上
|
|
848
|
+
markBoundary(s, id);
|
|
849
|
+
// 卡是这一刻新铸的 ⇒ 这一半的内容当然是「变了」(此前没有任何一份)
|
|
850
|
+
stampPhase(s, id, 'start', ev.eventId, true);
|
|
600
851
|
break;
|
|
601
852
|
}
|
|
602
853
|
case 'tool_end': {
|
|
603
854
|
const id = ev.toolCallId;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
855
|
+
// 卡在不在、在哪里,只按 `toolCallId` 现查 items —— **开着**与**已收口**两支从此共用同一条
|
|
856
|
+
// 定位与同一道闸(0.62.2 合并):此前开着那一支走的是另一张「开着的卡 → 下标」表、且**无条件**
|
|
857
|
+
// 落结果,于是同一件事有两处真源、两套规矩,而两条独立的流交错时旧收口正是从那一支进来的。
|
|
858
|
+
const at = id !== undefined ? toolCardIndexOf(s, id) : -1;
|
|
859
|
+
if (at >= 0) {
|
|
860
|
+
// 🔴 一道闸管两支:比卡上现在承载的那一阶段**旧、或就是同一条帧** ⇒ 整帧丢弃。
|
|
861
|
+
// · 同一条帧重放 ⇒ 再落一次会把结果字节重复计进预算,走到下面那支还会另铸一张已完成的卡;
|
|
862
|
+
// · 旧一阶段的收口(park 的 end 在恢复阶段之后才姗姗来迟)⇒ 会把屏上的真结果盖回去,
|
|
863
|
+
// 卡还开着时更会**关掉**一张属于新阶段、其实还在跑的卡(见 {@link staleEnd} 头注);
|
|
864
|
+
// · 反过来,**更新的一发**收口(门 park 掉这次调用、批复后恢复执行,上游按同一个
|
|
865
|
+
// `toolCallId` 再发一对 start/end)⇒ 后到的才是真结果,就地替换/落账。把「已 settled」
|
|
866
|
+
// 当成永久完成会把真结果吞掉,屏上停在那句「被门拦下」上。
|
|
867
|
+
// 🔴 身份缺席(判不出新旧)⇒ 闸不拦,照落(重放送的是同一份值,落账是空操作),同拍把
|
|
868
|
+
// `end` 那一半的水位清掉(见 {@link stampPhase})。
|
|
869
|
+
if (staleEnd(phaseOf(s, id), ev.eventId))
|
|
870
|
+
break;
|
|
871
|
+
const it = s.items[at];
|
|
872
|
+
const before = itemBytesOf(it);
|
|
873
|
+
const outBefore = it.output;
|
|
874
|
+
const errBefore = it.isError;
|
|
607
875
|
it.output = ev.output ?? '';
|
|
608
876
|
it.isError = ev.isError === true;
|
|
609
|
-
//
|
|
610
|
-
const
|
|
611
|
-
s.itemBytes[
|
|
612
|
-
addBytes(s,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// orphan close (start lost to the ring / arrived first) — synthesize a resolved card
|
|
618
|
-
pushItem(s, {
|
|
619
|
-
kind: 'tool',
|
|
620
|
-
id: id ?? `${ev.taskId}-tool-${s.items.length}`,
|
|
621
|
-
name: ev.toolName ?? 'Tool',
|
|
622
|
-
input: ev.args ?? {},
|
|
623
|
-
output: ev.output ?? '',
|
|
624
|
-
isError: ev.isError === true,
|
|
625
|
-
});
|
|
877
|
+
// 结果正文可能是**这一刻**才落到已在账的那条 item 上的 ⇒ 按差额补记(替换时差额可正可负)。
|
|
878
|
+
const delta = itemBytesOf(it) - before;
|
|
879
|
+
s.itemBytes[at] = (s.itemBytes[at] ?? 0) + delta;
|
|
880
|
+
addBytes(s, delta);
|
|
881
|
+
stampPhase(s, it.id, 'end', ev.eventId, it.output !== outBefore || it.isError !== errBefore);
|
|
882
|
+
break;
|
|
626
883
|
}
|
|
884
|
+
// orphan close (start lost to the ring / arrived first) — synthesize a resolved card.
|
|
885
|
+
// 🔴 这里**不**段闭合:段边界由 `tool_start` 给(收口先到时那一发迟到的 start 会补做,见上)。
|
|
886
|
+
// 在这里闭合会把**还在流的**那一段提前切成 item —— 每子代帽配得很紧时,那一段随即变成
|
|
887
|
+
// 「最早的内容」而被帽整只丢掉,而它其实是用户眼前正在看的字。
|
|
888
|
+
const synthId = id ?? `${ev.taskId}-tool-${s.items.length}`;
|
|
889
|
+
pushItem(s, {
|
|
890
|
+
kind: 'tool',
|
|
891
|
+
id: synthId,
|
|
892
|
+
name: ev.toolName ?? 'Tool',
|
|
893
|
+
input: ev.args ?? {},
|
|
894
|
+
output: ev.output ?? '',
|
|
895
|
+
isError: ev.isError === true,
|
|
896
|
+
});
|
|
897
|
+
// 🔴 边界虽然不在这里切,**位置**要在这里记下来:此刻缓冲里的那一截是「工具之前」那一段,
|
|
898
|
+
// 之后再流进来的是「工具之后」那一段。迟到的 `tool_start` 按这个位置切开两段(见
|
|
899
|
+
// {@link CardPhase.cut})—— 不记的话它只能把整条缓冲当成一段闭合,两段就此黏死。
|
|
900
|
+
recordCut(s);
|
|
901
|
+
// 卡是这一刻新铸的 ⇒ 结果那一半当然是「变了」
|
|
902
|
+
stampPhase(s, synthId, 'end', ev.eventId, true);
|
|
627
903
|
break;
|
|
628
904
|
}
|
|
629
905
|
}
|
|
@@ -3861,7 +3861,7 @@ readFaceDisagreement(capFace: unknown, posture: ReadFacePostureView | undefined)
|
|
|
3861
3861
|
|
|
3862
3862
|
| # | 件 | 面 | 破坏性 | 端要做什么 |
|
|
3863
3863
|
|---|---|---|---|---|
|
|
3864
|
-
| ① | 子代内容分流补 durable 聚合形 | 行为**修复** | 无 | 什么都不用做(自动生效);web 端自守的同款分流**可以退役**,判据见 26b |
|
|
3864
|
+
| ① | 子代内容分流补 durable 聚合形(0.62.1 再补工具两臂的幂等闸) | 行为**修复** | 无 | 什么都不用做(自动生效);web 端自守的同款分流**可以退役**,判据见 26b |
|
|
3865
3865
|
| ② | 子代内容账本字节预算 | additive 三导出 + 一测试钩 | 无 | 可选:启动时配值;渲染端**建议**接新的 `truncated` 槽位 |
|
|
3866
3866
|
| ③ | 座位 lane 子代两位 + 逐键出身表 | additive 两键 + 一导出 | 无 | 想要子代正文/子代续跑的端在 `sendMessage` 里带这两位 |
|
|
3867
3867
|
| ④ | 出站凭证位放宽成「串或取值函数」 | additive 型放宽 | 无 | 可选:凭证会变的端把串换成取值函数,不必再重建 client |
|
|
@@ -3896,11 +3896,93 @@ text?: string // 聚合两臂的整段全文(刻意不复用 delta 位)
|
|
|
3896
3896
|
库的处置是**吸收**:全文以当前缓冲为前缀 ⇒ 换掉而不是追加(同一条聚合帧重复送达因此**幂等**);
|
|
3897
3897
|
前缀对不上 ⇒ 退回追加、两段都留下(少渲一段比替引擎判定「哪一段才算数」更坏)。段闭合仍由工具帧/收口给。
|
|
3898
3898
|
|
|
3899
|
-
**durable 重放的两道幂等闸**(
|
|
3899
|
+
**durable 重放的两道幂等闸**(异源复审跟修;端若自己也在做同款分流,这两条一起才够):
|
|
3900
3900
|
①**事件身份** —— 聚合帧的 `eventId` 是精确的幂等键,**跨流也管用**(适配器内那道事件序号去重只在
|
|
3901
|
-
**同一条流**内有效,重连后是新的一条流);②**整段内容** —— 已段闭合上屏过的整段(
|
|
3902
|
-
|
|
3903
|
-
|
|
3901
|
+
**同一条流**内有效,重连后是新的一条流);②**整段内容** —— 已段闭合上屏过的整段(兜底:上游没给
|
|
3902
|
+
身份,或本进程这条流是重连后新起的)。另有第三形:被预算**裁过头部**的那一段,缓冲此时只剩尾巴,
|
|
3903
|
+
单看前缀会判成新内容整只追加回来 ⇒ 判据是「全文以缓冲结尾 ⇒ 同一段、整帧丢弃」。
|
|
3904
|
+
|
|
3905
|
+
🔴 **工具两臂同样有闸,身份换成 `toolCallId`**(**0.62.1 补**;端若自己也在做同款分流,这一条同样要):
|
|
3906
|
+
0.62.0 只给聚合两臂上了闸,`tool_start` / `tool_end` **一道都没有** —— 断线重连、durable 腿整轮重放时,
|
|
3907
|
+
同一次工具调用在子代查看态里排出**两张卡**,结果正文也被记两遍(内容字节预算跟着虚高)。0.62.1 起判据
|
|
3908
|
+
形与散文两臂相同(先问身份、再问账本),但**身份是 `toolCallId`、账本是查看态 `items` 上那张卡**,
|
|
3909
|
+
**刻意不共用聚合帧那本有界身份记号**:那本账的额度是按「聚合帧只在重放腿上来」定的,而工具帧**活体
|
|
3910
|
+
也来**,跟着写会把还在屏上的散文身份整窗挤掉 —— 修一个臂的重放去重、弄坏另一个臂的,不算修。
|
|
3911
|
+
逐条:
|
|
3912
|
+
- `tool_start` 见到「这个 `toolCallId` 的卡已在账」⇒ **不铸第二张**;
|
|
3913
|
+
- 但「卡在账」**不等于**「这一发 `tool_start` 处理过」—— 收口先到时库会合成一张占位卡(名 `Tool`、
|
|
3914
|
+
参数 `{}`),迟到/重放的 `tool_start` 带的才是真的 ⇒ **就地补齐名字与参数**(不覆盖已落的结果,
|
|
3915
|
+
字节按差额记账,重放多少次都不虚涨);段边界怎么给见下面那一条;
|
|
3916
|
+
- `tool_end` 只按 `toolCallId` **现查 `items`**:卡在账(不论开着还是已收口)走同一支、过同一道闸,
|
|
3917
|
+
卡不在账才合成。**0.62.2 改**:此前「开着」那一支走的是另一张「`toolCallId` → 开着的卡下标」表、
|
|
3918
|
+
且**无条件**落结果(理由写成「一张在飞的卡被拦掉收口就永远在飞」)—— 同一件事两处真源、两套规矩,
|
|
3919
|
+
而下面那条真病正是从那一支进来的;
|
|
3920
|
+
- **闸的判据是阶段的先后,不是「见过没见过」**:同一次调用真的会有**第二对**
|
|
3921
|
+
`tool_start`/`tool_end` —— 门把调用 park 掉、批复之后恢复执行时,引擎复用**同一个** `toolCallId`
|
|
3922
|
+
再发一对,第二对带的才是真结果(批复还可能**改过参数**,两次 start 的参数因此可以不同);
|
|
3923
|
+
- **阶段账**:库为每张卡记住它当下承载的那一阶段(已落账的 `tool_start` / `tool_end` 身份),
|
|
3924
|
+
按 `eventId` 的先后判新旧 —— 比它**旧或就是同一条帧** ⇒ 整帧丢弃(重放不重复计字节、旧一阶段
|
|
3925
|
+
不许把屏上的真结果或改后参数盖回去);**更新** ⇒ 后到的才算数,就地替换/更新(按差额记账)。
|
|
3926
|
+
🔴 **收口两半都比**(**0.62.2 改**):`tool_end` 除了跟 `end` 比,还要跟当前的 `start` 比 ——
|
|
3927
|
+
两条独立的流交错时(恢复阶段的 `tool_start` 已到、旧一阶段的 `tool_end` 才姗姗来迟),只比 `end`
|
|
3928
|
+
的话旧收口会落到新阶段那张**开着**的卡上并把它关掉,新参数配上旧阶段的失败结果,新流若中断这个
|
|
3929
|
+
错误状态就一直留在屏上。**更新的 `start` 已经建立了新的一阶段,早于它的收口不属于它**;
|
|
3930
|
+
「避免永远在飞」不构成接受一份已知过期结果的理由 —— 那张卡确实还在跑,显示在飞是**如实**的。
|
|
3931
|
+
🔴 **可比性的前提**:`eventId` 是上游铸的**时间可排序**句柄(uuidv7);端若自己造内容帧喂进来,
|
|
3932
|
+
这一位必须同样时间可排序,否则新旧判不出。**缺席** ⇒ 判不出 ⇒ 退回「后到的落账」(重放送的是
|
|
3933
|
+
同一份值,落账是空操作)。🔴 缺席的那一发**若真的改了**卡上那一半的内容,**同拍把对应那一半的
|
|
3934
|
+
记录清掉**(**0.62.2 改**):旧身份留着就是**替一份不再对应的内容背书** —— 实翻形是「先落新参数 +
|
|
3935
|
+
真结果(带身份),再收一发无身份的旧参数 / 失败结果,随后正确的那一发**原样重放**被自己的记录
|
|
3936
|
+
拦掉,卡永久停在旧内容上」。反过来,缺席的那一发**内容一个字节没变**(重放送来的是同一份值)⇒
|
|
3937
|
+
记录**原样保留** —— 它仍然如实描述卡上这份内容,清掉等于顺手把「旧一阶段不许盖新一阶段」那道闸
|
|
3938
|
+
一起卸了(那时旧 `start` 就能把改后参数盖回原参,而更新的结果还留在卡上)。一句话:**这一半的
|
|
3939
|
+
记录永远描述卡上这一半现在这份内容,内容没动它就没理由动**。阶段账与卡**同生共死**(卡被帽丢掉时
|
|
3940
|
+
一并清掉;清不掉的话卡被重建那一轮的收口会被陈旧记录整帧拦下 —— 卡建回来却永远显示在飞);
|
|
3941
|
+
- `tool_start` 除了不铸第二张卡,还负责**段边界**:每张卡记一位「**还欠不欠一次段闭合**」——
|
|
3942
|
+
欠着(= 它是收口先到时合成的占位卡,而自它出现以来一次段闭合都没发生)⇒ 这一发**先段闭合再补齐
|
|
3943
|
+
名参**;不欠了 ⇒ **不**段闭合。🔴 这一位问的是「**自这张卡出现以来发生过段闭合没有**」,不是
|
|
3944
|
+
「这张卡自己切过没有」(**0.62.2**):兑现一条边界的可以是别人(另一张卡的 `tool_start`、
|
|
3945
|
+
`settle`),所以**任何**一次段闭合都把在场每张卡的这一位置上;只记「自己切过没有」的话,那一发
|
|
3946
|
+
迟到的 `tool_start` 会在别人切完之后**再切一刀**,而这一刀切的是**当前**那条与它无关的活体尾段。
|
|
3947
|
+
🔴 同样**不是**拿「`start` 身份在不在」代答:身份是可选位,上游不给 `eventId` 时它永远抬不起来 ⇒
|
|
3948
|
+
每一次重放都会再切一刀。两种代答的坏结局是同一个:还在流的活体尾段被切成历史 item,随后该段的
|
|
3949
|
+
**聚合全文**在空缓冲上吸不到前缀、整只落回缓冲 ⇒ 前缀重复上屏;
|
|
3950
|
+
- `tool_end` 找不到卡(收口先于开始帧到达)⇒ 合成一张已完成的卡,**不**在那里段闭合 —— 边界由随后
|
|
3951
|
+
**第一次段闭合**补做(那一发迟到的 `tool_start` 只是其中一种触发者,见下)。在收口这一支闭合会把
|
|
3952
|
+
**还在流的**那一段提前切成一条历史 item,每子代帽配得很紧时它随即变成「最早的内容」被帽整只丢掉,
|
|
3953
|
+
而那正是用户眼前在看的字。🔴 但边界的**位置**
|
|
3954
|
+
在这一刻就记下来(**0.62.2 补**):此刻缓冲里的那一截是「工具之前」那一段,之后再流进来的是
|
|
3955
|
+
「工具之后」那一段,迟到的 `tool_start` 按这个位置把两段**分开**收。不记的话它只能把整条缓冲当成
|
|
3956
|
+
一段闭合 —— `AAA` → 收口先到 → `BBB` → 迟到的 start 这种乱序会合成 `AAABBB` 一段,整轮重放送来
|
|
3957
|
+
`AAA`、`BBB` 两段时两道内容闸都不命中,历史与缓冲各留一份。🔴 位置是**缓冲的**属性、**不是那张卡的
|
|
3958
|
+
私产**,库因此把它记在这条子代的账本上而不是卡上:①兑现它的不一定是那一发迟到的 `tool_start`
|
|
3959
|
+
(别的工具起头、`settle` 同样要切),任何一次段闭合都在**所有**还没兑现的位置上一起切 —— 一条缓冲上
|
|
3960
|
+
可以同时压着好几条(两张收口先到的卡交错),而闭合一发生缓冲就整只清空,没兑现的位置从此无处可切;
|
|
3961
|
+
②卡被条数帽/字节帽丢掉**不等于**缓冲被丢掉(帽先丢 item、丢不够才裁缓冲),那截正文一个字都没少,
|
|
3962
|
+
边界跟着卡消失就等于把「这里有过一次工具」这件事丢了;
|
|
3963
|
+
- 去重射程严格是**同一次调用**:另一个 `toolCallId` 的卡照常进(子代真的调了两次工具时两张卡都要在)。
|
|
3964
|
+
- 迟到的 `tool_start` 先段闭合、再找卡:段闭合本身可能触发条数裁剪(从头删卡 + 顶上留痕),所以库在 flush
|
|
3965
|
+
**之后**才按 `toolCallId` 定位那张卡;找不到 = 刚被裁掉 ⇒ 按「新的一次」重建(留白①),绝不对着裁剪前的
|
|
3966
|
+
下标/引用改名补参(否则差额记到留痕槽位上、字节账从此虚高)。
|
|
3967
|
+
|
|
3968
|
+
📋 **如实留白(工具臂)**:
|
|
3969
|
+
- ①卡在不在账的判据就是 `items` 自己(不另铸卡的记号账)—— 一张卡若已被条数帽/字节帽丢掉,同 id
|
|
3970
|
+
的帧按**新的一次**来处理:`tool_start` 与 `tool_end` **都还会再来**(整轮重放)时那张卡连同
|
|
3971
|
+
**最新那一阶段**的结果一起补回来;只剩收口帧的话补回来的卡缺参数,**只剩开始帧的话补回来的卡
|
|
3972
|
+
会显示成在飞**(结果那一发已经过去了,库不留它的内容)。这是刻意的:另开一本卡的记号账会在
|
|
3973
|
+
帽丢卡之后把重放永久挡住,屏上那一格从此空着且无人知道。
|
|
3974
|
+
- ②`toolCallId` 缺席的工具帧无从去重(库会为它合成一个位置相关的 id)—— wire 上的工具帧恒带这一位,
|
|
3975
|
+
这一格只对自造帧的宿主成立。
|
|
3976
|
+
- ③收口**先于**开始帧到达的那种乱序里,卡的位置是收口到达的位置(库不把它挪到开始帧那一刻),
|
|
3977
|
+
但它前后的散文段各自完整、各留一份 —— 包括**后一段先于迟到的 start 到达**那一形
|
|
3978
|
+
(`AAA` → 收口 → `BBB` → 迟到的 start):两段按收口到达那一刻的位置切开,各成一段、各进一条记号。
|
|
3979
|
+
- ④段边界记的是**位置**(收口到达那一刻缓冲的长度),不是缓冲的一份快照 —— 快照能有 MB 级,复制它
|
|
3980
|
+
等于把那份内容再占一遍而且不进字节账。位置的维护只有三处:缓冲**头部**被字节帽裁 ⇒ 同拍左移;
|
|
3981
|
+
段闭合 ⇒ 整表兑现后清空;别处缓冲只在尾部增长,前缀恒定。位置万一还是落到缓冲之外(端自造帧的
|
|
3982
|
+
乱序),库夹回去、退回**整条缓冲当一段**的老形(= 0.62.1 的行为),并且绝不劈开合法代理对
|
|
3983
|
+
(上游把一对代理对拆成两帧送时,记边界那一刻缓冲尾部可能正停在高位代理项上;撞上就把边界退到
|
|
3984
|
+
那一对之前)。两次段闭合之间最多记 **64** 条边界,满了之后再来的收口不再记(如实:那之后的边界会
|
|
3985
|
+
丢、两段黏一段;一次段闭合就整表清空,所以这只在「两次闭合之间来一长串收口先到的乱序帧」时才够得着)。
|
|
3904
3986
|
📋 **如实留白**:内容闸的代价是「一条 turn 里出现两段**逐字节相同**的正文时,durable 腿上只留一段」。
|
|
3905
3987
|
取舍明写:每次重连把整轮正文再渲一遍是**必然**发生的用户可见损坏,而同一条 turn 说两遍一模一样的话
|
|
3906
3988
|
是罕见形,且活体腿上两段都在。
|
|
@@ -4152,25 +4234,43 @@ function capForDisplay(raw: string, max: number): string
|
|
|
4152
4234
|
(不缺尾、也没有重复的前半段)。
|
|
4153
4235
|
- **G-11**:构造一个终帧与流式内容**从中途分岔**的场景 —— 屏上**不出现**拼接出来的第三种正文,且转录行
|
|
4154
4236
|
**改口**(不是一句「已完成」)。
|
|
4155
|
-
- **G-12
|
|
4156
|
-
|
|
4237
|
+
- **G-12**(0.62.1 判据订正,产品字节未动):让引擎/数据库回一个含控制字符的姿态值 —— 诊断行的
|
|
4238
|
+
显示长度**不超过该处的显示帽**,**行尾没有半截转义序列**,且**转义后仍在帽内的值一个字符都不截**
|
|
4239
|
+
(原样渲成完整的转义序列)。🔴 判据量一律是**转义后的显示长度**,不是原始字符数。
|
|
4240
|
+
🔴 原文写的是「不超过**同长度良性值**那一行」,那句话在**转义后仍在帽内**的那一档不成立、而且
|
|
4241
|
+
不成立是**设计**:呈前处理是**先转义、后按转义结果封长**,一个不可见字符渲成 6 个显示字符 ——
|
|
4242
|
+
那一档一枚都不用截,那一行因此**必然长于**同长度的良性行(良性 1:1)。照原文去实现的唯一办法是
|
|
4243
|
+
把展示行截到「原始字符数」那么长,而那会把整行诊断截没(实测:5 个控制字符 + 帽 40 ⇒ 空串)。
|
|
4244
|
+
「不比同长度良性行长」只在**两条输入都长到真的被封长**时成立,那是相对判据的射程,不是承诺。
|
|
4245
|
+
📐 三档一起验才不漏(帽 40 为例):5 个控制字符 ⇒ 转义后 30,**不截**;7 个控制字符 ⇒ 转义后 42
|
|
4246
|
+
**超帽**,截到 36(6 枚完整转义序列,原文只有 7 个字符也照截);6 个控制字符 + 33 个良性字符
|
|
4247
|
+
⇒ 恰 40,行尾是完整序列后的良性字符。
|
|
4157
4248
|
- **G-13**:一条**正常的多步回答**(散文 → 工具 → 散文)—— 转录里**不出现**任何「最终答复与显示的
|
|
4158
4249
|
不一致」类披露(拿整轮拼接去比会在这里假报)。
|
|
4159
4250
|
- **G-14**:活体跑完一整轮(两段散文 + 中间一张工具卡)之后**断线重连**、按 durable 腿整轮重放 ——
|
|
4160
|
-
|
|
4161
|
-
|
|
4251
|
+
子代查看态里每段只出现一次,**那张工具卡也只出现一次、结果只显示一次**,且顺序与活体时相同。
|
|
4252
|
+
🔴 重连是**新的一条流**,所以这一格必须真的断开重连来做:在同一条流里重放测不到它。
|
|
4253
|
+
反向一格(去重射程):同一轮里子代真的调了**两次不同**的工具 —— 两张卡都要在,不许被折成一张。
|
|
4254
|
+
四条边界一起验:①让某张工具卡被字节预算丢掉,再断线重连**整轮**重放(开始帧与收口帧都再来)——
|
|
4255
|
+
那张卡**连同最新那一阶段的结果**回来(不是空着,不是一张永远显示「在跑」的卡,也不是停在更早
|
|
4256
|
+
那一阶段的结果上);②让**结果先到、开始帧后到** —— 屏上只有一张卡,且它显示的是真的工具名与
|
|
4257
|
+
参数(不是占位),结果没被盖掉,这一次工具前后的两段散文各自完整、各留一份(重放之后也还是
|
|
4258
|
+
一份),而**还在流的**那一段不因为收口帧到达就被提前切成历史;③让一次调用**先被门拦下、批复后
|
|
4259
|
+
恢复执行**(引擎按同一个调用 id 再发一对开始/收口,批复时**改一下参数**)—— 屏上仍只有一张卡,
|
|
4260
|
+
显示的是**恢复后**的真结果与**改后**的参数;④随后把被拦下那一阶段的帧原样重放、以及让它**首次
|
|
4261
|
+
迟到**(乱序)—— 两种都**不许**把真结果或改后参数盖回去(「没见过」不等于「更新」)。
|
|
4162
4262
|
- **G-15**:把每子代帽配得比总帽还大 —— **响亮失败**(一个单条子代能整只越过的总帽不是预算)。
|
|
4163
4263
|
|
|
4164
4264
|
### 26k. 常驻门
|
|
4165
4265
|
|
|
4166
4266
|
| 门 | 守什么 |
|
|
4167
4267
|
|---|---|
|
|
4168
|
-
| 🆕 `scripts/run-subagent-durable-divert-test.mjs` | ① 聚合形分流:正控/负控/不双份三格/段边界/增量恒追加/**跨工具边界重放两道闸**(事件身份 + 整段内容) |
|
|
4268
|
+
| 🆕 `scripts/run-subagent-durable-divert-test.mjs` | ① 聚合形分流:正控/负控/不双份三格/段边界/增量恒追加/**跨工具边界重放两道闸**(事件身份 + 整段内容)/**工具两臂幂等闸**(跨流整轮重放卡恰一张 + 结果恰一次 + 异 `toolCallId` 负控 + 缺事件身份正控 + **帽淘汰后重放补回卡与结果** + **收口先到时迟来的开始帧补齐名参**) |
|
|
4169
4269
|
| 🆕 `scripts/run-subagent-content-budget-test.mjs` | ② 字节预算:每子代帽/总帽/截尾留痕(含进渲染计划)/缺省值可触到/配置面 fail-loud 八形/未越限零行为差/**拼接边界代理对不虚账**/**每子代帽 ≤ 总帽**/**被裁过的段重放不重算统计** |
|
|
4170
4270
|
| 🆕 `scripts/run-seat-task-request-origin-test.mjs` | ③ 座位载荷逐键出身对 sdk 型**直读**对账 + 子代两位回归钉 + 保留窗口内层两键 |
|
|
4171
4271
|
| 🆕 `scripts/run-wire-auth-source-test.mjs` | ④ 取值时机:构造期零读 / 读取次数 = 请求次数 / 改值即生效 / 三态逐格 / 凭证只在 Authorization 头 / 抛错 fail-soft |
|
|
4172
4272
|
| 🆕 `scripts/run-result-text-backfill-test.mjs` | ⑥ 补差四格 + 事实位形 + 跨工具边界格 + **正常多步回答零分岔** + **首尾空白零分岔**(真链路驱动,终帧按引擎真形造) |
|
|
4173
|
-
| 🆕 `scripts/run-display-cap-order-test.mjs` | ⑦ 封长顺序四铸点 + 共用铸点两条避让 + 反向参照 |
|
|
4273
|
+
| 🆕 `scripts/run-display-cap-order-test.mjs` | ⑦ 封长顺序四铸点 + 共用铸点两条避让 + 反向参照 + **转义后在帽内不截 / 原文短而转义超帽照截 / 恰好抵帽**三档(G-12 订正后的判据) |
|
|
4174
4274
|
| `scripts/run-esc-halt-plan-test.mjs` | ⑤ 新增 G6/G6b 两段(detach 格 + 缺席零差全格穷举) |
|
|
4175
4275
|
| `scripts/run-public-surface-test.mjs` | 公面导出基线 **864 → 869**(测试钩 43 → 44) |
|
|
4176
4276
|
| `scripts/run-integration-doc-freshness-test.mjs` | 本节新增坐标在盘 + 入库;§0a/§2/§2b 计数与基线等值 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.62.
|
|
3
|
+
"version": "0.62.2",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|