@sema-agent/client-core 0.67.2 → 0.68.1
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 +310 -0
- package/README.md +65 -1
- package/dist/adapt/arms.js +67 -9
- package/dist/adapt/textStream.d.ts +102 -1
- package/dist/adapt/textStream.js +169 -6
- package/dist/adapt/turnFlags.d.ts +14 -0
- package/dist/adapt/turnFlags.js +4 -1
- package/dist/adapt.js +4 -1
- package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
- package/dist/adapter/activeRunSelfHeal.js +79 -8
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +50 -9
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +29 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +46 -15
- package/dist/adapter/runStream.d.ts +22 -2
- package/dist/adapter/runStream.js +139 -38
- package/dist/adapter/types.d.ts +4 -1
- package/dist/autoModeUnavailable.d.ts +17 -9
- package/dist/autoModeUnavailable.js +26 -8
- package/dist/classifierStatus.d.ts +32 -4
- package/dist/classifierStatus.js +5 -3
- package/dist/controlRouter.d.ts +16 -0
- package/dist/controlRouter.js +6 -0
- package/dist/engineErrorCodes.d.ts +52 -0
- package/dist/engineErrorCodes.js +117 -0
- package/dist/engineNoticeCodes.d.ts +95 -1
- package/dist/engineNoticeCodes.js +124 -1
- package/dist/gateVocabulary.d.ts +18 -7
- package/dist/gateVocabulary.js +21 -8
- package/dist/hitl/parkResolver.d.ts +0 -14
- package/dist/hitl/parkResolver.js +22 -9
- package/dist/hitl/toolApprovalWire.d.ts +2 -1
- package/dist/hitl/toolApprovalWire.js +1 -0
- package/dist/ownKey.d.ts +34 -0
- package/dist/ownKey.js +36 -0
- package/dist/request/taskRequest.d.ts +6 -6
- package/dist/request/taskRequest.js +45 -0
- package/dist/retryStatus.d.ts +13 -2
- package/dist/retryStatus.js +4 -1
- package/dist/runTerminal.d.ts +87 -14
- package/dist/runTerminal.js +89 -15
- package/dist/seam.d.ts +78 -8
- package/dist/seam.js +16 -2
- package/dist/toolResult.js +8 -0
- package/dist/toolRoster.d.ts +34 -2
- package/dist/toolRoster.js +16 -2
- package/dist/workflowClient.d.ts +22 -0
- package/dist/workflowClient.js +37 -0
- package/docs/INTEGRATION-CLIENTS.md +633 -9
- package/package.json +2 -2
|
@@ -15,13 +15,64 @@
|
|
|
15
15
|
* 三个 take/drain 的**相对次序**由调用点决定,本模块只保证每一个自身的内部序;拆分后
|
|
16
16
|
* 收口三形(result 臂 / abort 尾 / T60 尾)的次序差异登记在 ADAPT-RW-MATRIX §2.4,别归一。
|
|
17
17
|
*
|
|
18
|
+
* 🔴 0.68.1 / L-310 追加的**三行状态**(`segmentCommitted` = 当前引擎段里已经撤不回的那一截 /
|
|
19
|
+
* `previousSegmentCommitted` = 上一个包侧边界带走的那一截,迟到的段边界靠它算分歧 /
|
|
20
|
+
* `segmentSealed` = 已由引擎明报收尾、但还没提交的定稿正文)与第七个动作
|
|
21
|
+
* {@link TextStream.replaceAnswerSegment}(`text_end.content` 的权威段替换)。上面那句「12 个变量」
|
|
22
|
+
* 是 REF-CC-SPLIT-02 收编当时的实数,不是上限 —— 新增那三行各有自己的声明处头注,
|
|
23
|
+
* 数目与理由一起记明(封闭性声明必须机械验证,别拿一个对不上的数字自证)。
|
|
24
|
+
*
|
|
18
25
|
* 🔴 时序坑(矩阵 §2.3 ⑤,拆分专属):`drainLive()` 里两处 `pending = ''` 在 `yield` **之后**,
|
|
19
26
|
* 载荷是在 yield 之前**按值捕获**的,所以逻辑正确。**机械地把清空前移会读到已清空的串** ——
|
|
20
27
|
* 谁想「统一成 reset-before-yield」,先读这一条。
|
|
21
28
|
*/
|
|
22
29
|
import type { AdapterContext, AdapterOutput } from '../seam.js';
|
|
23
30
|
import { type Frame, type IdOf } from './ids.js';
|
|
24
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* {@link TextStream.replaceAnswerSegment} 的回执 —— **三件事实**,一个字节的正文都不复述。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 三位**互不替代**(L-310 设计定谳,2026-09-14):
|
|
35
|
+
* · {@link diverged} 回答「屏上那份对不对」;
|
|
36
|
+
* · {@link committedPrefixLen} 回答「这一段有多少字节**已经撤不回**了」(idle-flush 铸过的
|
|
37
|
+
* transcript 消息早已 yield 给宿主);
|
|
38
|
+
* · {@link committedPrefixDiverged} 回答「撤不回的那一截**自己**是不是也被改过」——
|
|
39
|
+
* 它为真就是本件要防的最坏一形:**未脱敏字节已经进了本地转录**,包内修不了,只有宿主重渲。
|
|
40
|
+
*/
|
|
41
|
+
export interface TextSegmentReplacement {
|
|
42
|
+
/** 替换前按 delta 拼出来的本段全文 ≠ 权威全文。假 = 逐字节相同(旧引擎 / 不含凭据形的文本)。 */
|
|
43
|
+
diverged: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 本段在替换发生前**已经 committed 上屏**的长度(idle-flush 的那些刀 + 包侧边界带走的那些截)。
|
|
46
|
+
*
|
|
47
|
+
* 🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节,也不是「几条消息」**
|
|
48
|
+
* (异源对抗复审轮三 finding① 订正 —— 此前文案写「字节」,宿主按 UTF-8 字节去截会在任何非
|
|
49
|
+
* ASCII 正文上截错位置:实测 `'sk-…' + '中文'×30 + '.\n'` 的 UTF-16 长度 82 / UTF-8 字节 202,
|
|
50
|
+
* 按 202 截会把凭据**原样留在屏上**还附带乱码)。它就是给 `String.prototype.slice` 用的那个数:
|
|
51
|
+
* `已committed正文.slice(0, 已committed正文.length - committedPrefixLen) + content`。
|
|
52
|
+
* 🔴 **不是「几条消息」**:本包在 idle-flush 那一形下会把「上一段的定稿 + 这一段的半截」合并进
|
|
53
|
+
* **同一条**消息(分段行为零改动的代价),按**消息**去丢会把已经定稿的上一段一起删掉,而本包
|
|
54
|
+
* 不会再补发它。
|
|
55
|
+
* 🔴 它按**替换前的拼文**计长,**不是** `content` 上的偏移量 —— 前缀自己也可能被脱敏改过字节
|
|
56
|
+
* (见 {@link committedPrefixDiverged});拿它去切 `content` 在那一形上会切出半截乱码。
|
|
57
|
+
*/
|
|
58
|
+
committedPrefixLen: number;
|
|
59
|
+
/**
|
|
60
|
+
* 已提交的那一截**本身**与权威全文对不上(= 凭据落在已经上屏的那一截里)。
|
|
61
|
+
* 🔴 **统一规则**:这一位为真时本模块**一个字节都不再交**给转录面 —— 已提交那截撤不回,而切片
|
|
62
|
+
* 拼不出 `content`,再补一条就是同一段话上屏两遍。该段唯一算数的那一份在帧上的 `content` 里,
|
|
63
|
+
* 宿主用它换掉 {@link committedPrefixLen} 指的那一截(所以 chrome 臂上这一位是
|
|
64
|
+
* never-false 的显形键,不是内部量)。
|
|
65
|
+
* 🔴 这一形下、且**这一截整个都在当前这条 assistant 消息里**时,本模块同批把终答补差账
|
|
66
|
+
* ({@link TextStream.committedAnswerText})同步成「宿主照契约换过之后屏上有什么」——
|
|
67
|
+
* 不同步的话 `result` 臂会把权威全文当成「屏上缺的后缀」**再渲一遍**(轮三 finding② 实抓)。
|
|
68
|
+
* ⚠️ **段跨过包侧边界(工具卡 / 消息划界)时刻意不碰账**:那一截的字节落在**别的**消息里,而两本
|
|
69
|
+
* 补差账是**按消息**记的,写进当前这本就是把别条消息的正文算进这条(轮四 finding①② 实抓的两种
|
|
70
|
+
* 翻车)。那几形的补差基线与「换过之后的屏」对不齐 —— **这是本批之前就有的行为**(主树同条
|
|
71
|
+
* 时序实测同形),根因是包/引擎两套边界对不齐,根治见 `seam.ts` 义务① 末尾那条留白。
|
|
72
|
+
*/
|
|
73
|
+
committedPrefixDiverged: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** M1 对外的七个动作 + 两个读位(矩阵 §3.2 的 #2/#3 两条跨模块接口就是最后那三件)。 */
|
|
25
76
|
export interface TextStream {
|
|
26
77
|
/** A5 `text_delta` 半场:MOD-1 思考→回答边界(思考在场就先 committed 上屏)+ 段锚 + 三缓冲累加。 */
|
|
27
78
|
feedText(delta: string, frame: Frame): Generator<AdapterOutput>;
|
|
@@ -43,6 +94,56 @@ export interface TextStream {
|
|
|
43
94
|
* 两条都不满足时**什么都不做**(缓冲原样押着,活体预览照常由 `drainLive` 走)。
|
|
44
95
|
*/
|
|
45
96
|
takeAnswerSegmentOnIdle(): Generator<AdapterOutput>;
|
|
97
|
+
/**
|
|
98
|
+
* L-310 —— 用引擎报的**段权威全文**整段替换本段已攒的 delta 拼文(server ≥7.75.3)。
|
|
99
|
+
*
|
|
100
|
+
* ## 为什么要有它(病形一句话)
|
|
101
|
+
* server 7.75.3 起 `text_end.content` 走的是与 `result`/账本同一只**脱敏器**,而 `text_delta`
|
|
102
|
+
* 仍逐字(跨 chunk 的凭据无法就地判)⇒ 两者**可以不相等**。本包此前只把 `content` 当段界信号,
|
|
103
|
+
* 于是屏上的活体流与 transcript 平面的 committed 消息用的都是 delta 拼文 —— 一段含凭据的回答
|
|
104
|
+
* 以**未脱敏**的字节留在本地转录里。契约原文(server `ASSISTANT-WIRE-CONTRACT` §5.1 live 面)
|
|
105
|
+
* 逐字:「消费端在 `text_end` 到达时应以它**整段替换**已攒的 delta,而不是只当段界信号」。
|
|
106
|
+
*
|
|
107
|
+
* ## 四形怎么处置(每一形都是**判断**,不是顺手)
|
|
108
|
+
* (a) **`text_end` 先于段提交**(常态):整段还押在缓冲里 ⇒ 段缓冲整段换成 `content`,
|
|
109
|
+
* 下一次 {@link TextStream.takeAnswerSegment} 铸出来的 committed 文本就是权威全文。
|
|
110
|
+
* (b) **半段已被 idle-flush 提交**:那条 transcript 消息**早已 yield 给宿主**,包内撤不回。
|
|
111
|
+
* · 已提交前缀逐字对得上 ⇒ 只封存**未提交尾段**(`content` 去掉前缀那一截),两条 committed
|
|
112
|
+
* 拼起来逐字节 = `content`;
|
|
113
|
+
* · 已提交前缀**自己**就对不上(凭据落在它里面)⇒ **一个字节都不再交**
|
|
114
|
+
* ({@link TextSegmentReplacement.committedPrefixDiverged} 为真),该段唯一算数的那一份在帧上,
|
|
115
|
+
* 由宿主重渲并丢掉此前那些消息 —— 「整条 committed 消息重铸」只能由宿主做(它才持有那条
|
|
116
|
+
* 消息的句柄),包能做的是**把这件事说出来**。
|
|
117
|
+
* 判据锚在 `content.startsWith(<已提交前缀>)`,不锚「有没有 idle-flush 过」——
|
|
118
|
+
* 后者是前置条件,前者才是真正决定结果的量。
|
|
119
|
+
* (c) **一 turn 多段**(anthropic 车道常态:逐 `content_block_stop` 发 `text_end`):定稿正文
|
|
120
|
+
* **封存**、`answerSegment` 清空给下一段用。🔴 不清空 = 下一段的 delta 接着往同一个缓冲里加、
|
|
121
|
+
* 再被下一帧的 `content` 整段覆盖 ⇒ **上一段的正文凭空消失**,且下一帧还会误报 `diverged`
|
|
122
|
+
* (异源对抗复审实抓)。封存**不切消息**:整 turn 仍只提交一条 assistant 文本消息。
|
|
123
|
+
* (d) **子流**(`parentToolCallId` 在场):根本到不了这里 —— 臂在 `arms.ts` 就断闸了
|
|
124
|
+
* (拿子代的段边界去改 leader 的缓冲 = 跨 lane 状态破坏)。
|
|
125
|
+
* (e) 🔴 **迟到的段边界**(openai 车道实测时序:`text_end` 在 `finalize()` 里 push,而
|
|
126
|
+
* `toolcall_end` 先触发 `executor.maybeAdmit` ⇒ **工具卡先到、段边界后到**):那一拍卡前散文
|
|
127
|
+
* 已被 `takeAnswerSegment()` 提交进**上一条**消息,两个缓冲双双为空。据
|
|
128
|
+
* {@link previousSegmentCommitted} **照算分歧、照发信号**,但**不铸任何消息**(那些字节在
|
|
129
|
+
* 上一条消息里,在这里补一条只会落到工具卡**之后**)。修前这一形被当成 (f) 全否返回 ⇒
|
|
130
|
+
* 一段未脱敏正文静静留在转录里、三个键一个都不发(异源对抗复审实抓)。
|
|
131
|
+
* (f) 🔴 **本包一个字节都没经手这一段**(两本前缀账与段缓冲全空):**什么都不做**,三位全报否。
|
|
132
|
+
* 替换的语义是「换掉本包自己缝合出来的那一段」,不是「凭空铸一段」—— durable 内容腿
|
|
133
|
+
* (整块 `text` 帧,本身就是一整条 assistant 消息)之后再来一帧 `text_end`,塞进缓冲就会在
|
|
134
|
+
* turn 收口**再铸一条一模一样的**。红先绿后实抓(`run-client-core-pure-test` `#323-g`)。
|
|
135
|
+
*
|
|
136
|
+
* ## 活体面(`textPending`)按替换后的段重算
|
|
137
|
+
* 已经泄出去的那一截(`stream_delta`)**撤不回**。所以:已泄前缀仍是 `content` 的前缀 ⇒ 尾巴
|
|
138
|
+
* 换成 `content` 剩下那一截(屏上接着往下写,零重复);已泄前缀**不是** `content` 的前缀 ⇒
|
|
139
|
+
* **不再补任何尾巴**(在一段错的前缀后面接上权威后缀,拼出来的是一段谁都没说过的话),
|
|
140
|
+
* 由宿主按 `diverged` 重渲整段。
|
|
141
|
+
*
|
|
142
|
+
* @param content 该段的权威全文(UNTRUSTED、仅展示,与 `text_delta` 同契约)。
|
|
143
|
+
* @param anchor 本帧 —— 段锚此刻为空(整段已被 idle 提交光 / 整段零 delta)时用它当 committed
|
|
144
|
+
* 消息的 id 锚;段锚已在场时**不覆盖**(重放确定性:同流同 id)。
|
|
145
|
+
*/
|
|
146
|
+
replaceAnswerSegment(content: string, anchor?: Frame): TextSegmentReplacement;
|
|
46
147
|
/** D3 IDLE-FLUSH 竞速判据:四个缓冲有任何一个非空。 */
|
|
47
148
|
hasPendingContent(): boolean;
|
|
48
149
|
/** D5 攒批节拍:一个间隔至多一次 flush(判据与 `lastFlushAt` 一起收在本模块内)。 */
|
package/dist/adapt/textStream.js
CHANGED
|
@@ -62,6 +62,46 @@ export function createTextStream(ctx, idOf) {
|
|
|
62
62
|
* 「工具帧来过」本身就是重开新段的事实,与那一拍有没有散文可提交无关。
|
|
63
63
|
*/
|
|
64
64
|
let idleFlushUsedInSegment = false;
|
|
65
|
+
/**
|
|
66
|
+
* L-310:**当前引擎段**里已经 committed 上屏的那一截(idle-flush 的刀)。
|
|
67
|
+
*
|
|
68
|
+
* 🔴 与 `committedText` 刻意分家(两个量、两件事,别合并):`committedText` 记的是「**这条
|
|
69
|
+
* assistant 消息**屏上已经有了哪些字」(跨段累加,补差判据的输入);这一个记的是「**这一段**
|
|
70
|
+
* 有多少字节已经撤不回了」——它是权威段替换唯一能算出「尾段该换成什么」的量。
|
|
71
|
+
* 写于 `emitAnswerSegment`(提交本体,**只计当前段那一截**,不含 {@link segmentSealed});
|
|
72
|
+
* 归零于三处**段到此为止**的事实发生地:正常边界(`takeAnswerSegment`)、权威替换
|
|
73
|
+
* (`replaceAnswerSegment`)、新消息划界(`beginAssistantMessage`)。
|
|
74
|
+
* idle 入口**不**归零 —— 那正是「段还没完、只是先上屏半截」这件事本身。
|
|
75
|
+
*/
|
|
76
|
+
let segmentCommitted = '';
|
|
77
|
+
/**
|
|
78
|
+
* 🔴 **上一个包侧边界带走的那一截**(L-310 / 异源复审 finding①,0.68.1)。
|
|
79
|
+
*
|
|
80
|
+
* 存在的理由是一条**上游实测的时序**:openai 车道的 `text_end` 是在 `finalize()` 里 push 的
|
|
81
|
+
* (装机 core `dist/brain/openai.js` 的 `finalize` 真字节),而 `toolcall_end` 会先触发
|
|
82
|
+
* `executor.maybeAdmit`(`dist/engine/loop/agent-loop.js`)⇒ **工具卡先到、段边界后到**。
|
|
83
|
+
* 那一拍包侧的工具卡臂已经 `takeAnswerSegment()` 把卡前散文提交进**上一条** assistant 消息,
|
|
84
|
+
* 于是 `segmentCommitted` 与 `answerSegment` 双双为空 —— 若据此判「本包没经手过这一段」,
|
|
85
|
+
* 一段**未脱敏**的正文就会静静留在转录里、且三个分歧键一个都不发(宿主连纠正的机会都没有)。
|
|
86
|
+
* ⇒ 边界带走那一截时把它记在这里,迟到的 `text_end` 据它**算分歧、发信号**;
|
|
87
|
+
* 但**不铸任何消息**(那些字节在上一条消息里,在这里补一条只会落到工具卡**之后**)。
|
|
88
|
+
* 与 `previousCommittedText` 是同一条思路的两个量,刻意分开:那一个是「上一条消息的全部正文」
|
|
89
|
+
* (终答补差用),这一个是「上一个包侧边界带走的**本段**那一截」(分歧判据用)。
|
|
90
|
+
*/
|
|
91
|
+
let previousSegmentCommitted = '';
|
|
92
|
+
/**
|
|
93
|
+
* 🔴 **已由引擎明报收尾、但还没提交的段正文**(L-310 / 异源复审 finding②,0.68.1)。
|
|
94
|
+
*
|
|
95
|
+
* 病形(复审实抓):`delta(A) → text_end(A) → delta(B) → text_end(B)` 中间没有工具卡也没有
|
|
96
|
+
* idle 提交时,若替换只是把 `content` 写回 `answerSegment`,B 的 delta 会**接着往同一个缓冲里加**,
|
|
97
|
+
* 然后被 `contentB` 整段覆盖 —— **A 段的正文凭空消失**,而且 B 那一帧还会误报 `diverged`。
|
|
98
|
+
* 这条时序在 anthropic 车道是**常态**(装机 core `dist/brain/anthropic.js` 在
|
|
99
|
+
* `content_block_stop` 上逐块 push `text_end`,一条消息可以有多个 text 块)。
|
|
100
|
+
* ⇒ 替换把定稿正文移进本缓冲(「封存」),`answerSegment` 清空给下一段用;段提交本体交的是
|
|
101
|
+
* `segmentSealed + answerSegment`。分段行为**零改动**(整 turn 仍只提交一条 assistant 文本
|
|
102
|
+
* 消息,除非有工具卡/idle 边界)—— 撤 idle-flush 启发式仍是另一批的事。
|
|
103
|
+
*/
|
|
104
|
+
let segmentSealed = '';
|
|
65
105
|
/** T29 攒批节拍:宿主策略优先(桌面/web 可传 16ms 对齐 CC 桌面端),缺席=207 CLI 口径 100ms。 */
|
|
66
106
|
const flushEveryMs = typeof ctx.coalesceIntervalMs === 'number' && ctx.coalesceIntervalMs >= 0
|
|
67
107
|
? ctx.coalesceIntervalMs
|
|
@@ -93,18 +133,24 @@ export function createTextStream(ctx, idOf) {
|
|
|
93
133
|
/** 段提交的**唯一本体**(两个入口 `takeAnswerSegment` / `takeAnswerSegmentOnIdle` 共用;
|
|
94
134
|
* 两个入口的差别只在准入判据,提交出来的消息一个字节都不分叉)。 */
|
|
95
135
|
function* emitAnswerSegment() {
|
|
96
|
-
|
|
136
|
+
// L-310:提交的是「已封存的定稿段 + 还在攒的当前段」。分段行为零改动 —— 封存只换字节,不切消息。
|
|
137
|
+
const body = segmentSealed + answerSegment;
|
|
138
|
+
if (body.length === 0)
|
|
97
139
|
return;
|
|
98
140
|
const anchor = segmentAnchor ?? {};
|
|
99
141
|
const msg = {
|
|
100
142
|
type: 'assistant',
|
|
101
143
|
// L-215③:同 takeThinking —— 身份位接力(帧优先、ctx 兜底;两位都读不出就一个都不铸)。
|
|
102
|
-
message: { role: 'assistant', content: [{ type: 'text', text:
|
|
144
|
+
message: { role: 'assistant', content: [{ type: 'text', text: body }], ...messageIdentityOf(anchor, ctx) },
|
|
103
145
|
uuid: idOf(anchor, 'text'),
|
|
104
146
|
session_id: ctx.sessionId,
|
|
105
147
|
parent_tool_use_id: null,
|
|
106
148
|
};
|
|
107
|
-
committedText +=
|
|
149
|
+
committedText += body;
|
|
150
|
+
// L-310:🔴 **只有当前段那一截**算进「本段已提交前缀」—— 已封存的定稿段属于**上一个**引擎段,
|
|
151
|
+
// 把它算进来会让下一帧 `text_end` 拿一个跨段的前缀去比 `content`,当场误报前缀分歧。
|
|
152
|
+
segmentCommitted += answerSegment;
|
|
153
|
+
segmentSealed = '';
|
|
108
154
|
answerSegment = '';
|
|
109
155
|
segmentAnchor = null;
|
|
110
156
|
emittedAssistantText = true;
|
|
@@ -116,14 +162,22 @@ export function createTextStream(ctx, idOf) {
|
|
|
116
162
|
function* takeAnswerSegment() {
|
|
117
163
|
idleFlushUsedInSegment = false;
|
|
118
164
|
yield* emitAnswerSegment();
|
|
165
|
+
// L-310:正常边界带走了当前段的那一截 ⇒ 本段前缀账归零(下一段的替换不许把上一段的字节
|
|
166
|
+
// 算成自己的前缀),但**记进** `previousSegmentCommitted` —— 这是包侧边界,不是引擎段边界:
|
|
167
|
+
// openai 车道的 `text_end` 会在这之后才到,那一拍还得拿它算分歧(见 replaceAnswerSegment)。
|
|
168
|
+
previousSegmentCommitted += segmentCommitted;
|
|
169
|
+
segmentCommitted = '';
|
|
119
170
|
}
|
|
120
171
|
/** 件 A:IDLE-FLUSH 专用入口(准入 = 每段一刀 + 句末/段末边界;两条都过才提交)。 */
|
|
121
172
|
function* takeAnswerSegmentOnIdle() {
|
|
122
|
-
|
|
173
|
+
// L-310:准入判的是**提交本体真要交的那份**(封存段 + 当前段),不是只看当前段 ——
|
|
174
|
+
// 否则「定稿段已封存、当前段还空着」那一拍会被判成没东西可交,把已定稿的正文继续押着。
|
|
175
|
+
const body = segmentSealed + answerSegment;
|
|
176
|
+
if (body.length === 0)
|
|
123
177
|
return;
|
|
124
178
|
if (idleFlushUsedInSegment)
|
|
125
179
|
return;
|
|
126
|
-
if (!endsAtSentenceBoundary(
|
|
180
|
+
if (!endsAtSentenceBoundary(body))
|
|
127
181
|
return;
|
|
128
182
|
idleFlushUsedInSegment = true;
|
|
129
183
|
yield* emitAnswerSegment();
|
|
@@ -168,7 +222,8 @@ export function createTextStream(ctx, idOf) {
|
|
|
168
222
|
yield* drainLive();
|
|
169
223
|
yield* takeThinking();
|
|
170
224
|
}
|
|
171
|
-
|
|
225
|
+
// L-310:已封存的定稿段也算「这条消息已经开了头」⇒ 锚不重置(同流重放同 id)。
|
|
226
|
+
if (answerSegment.length === 0 && segmentSealed.length === 0)
|
|
172
227
|
segmentAnchor = frame;
|
|
173
228
|
answer += delta;
|
|
174
229
|
answerSegment += delta;
|
|
@@ -188,7 +243,111 @@ export function createTextStream(ctx, idOf) {
|
|
|
188
243
|
takeThinking,
|
|
189
244
|
takeAnswerSegment,
|
|
190
245
|
takeAnswerSegmentOnIdle,
|
|
246
|
+
replaceAnswerSegment: (content, anchor) => {
|
|
247
|
+
const prevSegment = answerSegment;
|
|
248
|
+
/**
|
|
249
|
+
* 本段**已经撤不回**的那一截 = 包侧边界带走的(可能跨了工具卡 / 消息划界,见
|
|
250
|
+
* {@link previousSegmentCommitted})+ 本消息内 idle-flush 掉的。
|
|
251
|
+
* 🔴 **两本账必须相加,不是二选一**(异源对抗复审轮二 finding① 实抓):修前只在「当前缓冲
|
|
252
|
+
* 全空」时才去读前一本,于是 `delta(含凭据) → 工具卡 → 同段后续 delta → text_end` 这条
|
|
253
|
+
* **openai 车道真实交错**(brain 持续累加文本直到 `finalize`,而 streamingToolExecution
|
|
254
|
+
* 允许工具先执行)上,卡前那截明文**不算进前缀** ⇒ 两个前缀键双双缺席、旧明文消息留存,
|
|
255
|
+
* 而权威全文又被完整提交一次(同一段话上屏两遍 + 明文没人清)。实测复现过。
|
|
256
|
+
*/
|
|
257
|
+
const livePrefix = previousSegmentCommitted + segmentCommitted;
|
|
258
|
+
// 🔴 **本包一个字节都没经手这一段 ⇒ 什么都不做**(形 (f);#323-g 那条正控守的就是这一形)。
|
|
259
|
+
// 可达形两条,都不是「流式段边界」:① durable 内容腿(`assistant` 臂的整块 `text`)——
|
|
260
|
+
// 那一帧本身就是一整条 assistant 消息、已经铸过 transcript 行,随后到的 `text_end` 若在这里
|
|
261
|
+
// 把 `content` 塞进段缓冲,turn 收口就会**再铸一条一模一样的**;② 宿主只喂 `text_end`、
|
|
262
|
+
// 不喂 `text_delta` 的自建管线。共同事实 = 没有可替换的对象,而替换的语义是**换掉本包自己
|
|
263
|
+
// 缝合出来的那一段**,不是「凭空铸一段」。
|
|
264
|
+
// (durable 腿本身不缺脱敏:server 的 `text` 行在写口与读口各脱一次,见契约 §5.1。)
|
|
265
|
+
if (livePrefix.length === 0 && prevSegment.length === 0) {
|
|
266
|
+
return { diverged: false, committedPrefixLen: 0, committedPrefixDiverged: false };
|
|
267
|
+
}
|
|
268
|
+
const liveSegment = livePrefix + prevSegment;
|
|
269
|
+
const diverged = liveSegment !== content;
|
|
270
|
+
const committedPrefixLen = livePrefix.length;
|
|
271
|
+
const committedPrefixDiverged = committedPrefixLen > 0 && !content.startsWith(livePrefix);
|
|
272
|
+
/**
|
|
273
|
+
* 🔴 **段跨过包侧边界、而且已提交那截自己也过期** ⇒ 本模块**一个字节都不动**(只发信号)。
|
|
274
|
+
*
|
|
275
|
+
* 理由是**不许把一条既有行为改坏**(异源对抗复审轮五 finding② 实抓,基线对照确认):这一形下
|
|
276
|
+
* 那一截的字节在**别的** assistant 消息里,而本模块能动的只有当前这条 —— 清掉未提交尾段会让
|
|
277
|
+
* `lastCommittedAnswerText` 回退到上一条,`result` 臂据它落第④臂**补吐一整段**;而 e4cd371
|
|
278
|
+
* 基线上同一条时序只是提交那截尾段 + 落第⑤臂报一行分岔(不补吐)。⇒ 清空在这里是**新增**的
|
|
279
|
+
* 重复转录路径,不是「更安全」。
|
|
280
|
+
* ⇒ 分界线一句话:**整段都在自己手里(同一条消息内)时才改自己交的字节;段一旦跨过包侧边界,
|
|
281
|
+
* 就只发信号、不动任何既有行为**。宿主照三个键把那截过期前缀换掉,其余与 0.68.0 逐字相同。
|
|
282
|
+
*/
|
|
283
|
+
const handOffOnly = committedPrefixDiverged && previousSegmentCommitted.length > 0;
|
|
284
|
+
if (handOffOnly) {
|
|
285
|
+
segmentCommitted = '';
|
|
286
|
+
previousSegmentCommitted = '';
|
|
287
|
+
idleFlushUsedInSegment = false;
|
|
288
|
+
return { diverged, committedPrefixLen, committedPrefixDiverged };
|
|
289
|
+
}
|
|
290
|
+
// ── ① 活体面:已泄出去的那一截撤不回 ⇒ 尾巴按替换后的段重算(判据见接口注「活体面」段)──
|
|
291
|
+
// `textPending` 是段缓冲的**后缀**;工具卡边界只提交不 drain,所以它也可能还押着**上一段**
|
|
292
|
+
// 的尾巴(那一截与本次替换无关,原样留着)。
|
|
293
|
+
const segTailLen = Math.min(textPending.length, prevSegment.length);
|
|
294
|
+
const carry = textPending.slice(0, textPending.length - segTailLen);
|
|
295
|
+
const emittedForSegment = livePrefix + prevSegment.slice(0, prevSegment.length - segTailLen);
|
|
296
|
+
textPending = content.startsWith(emittedForSegment)
|
|
297
|
+
? carry + content.slice(emittedForSegment.length)
|
|
298
|
+
: carry;
|
|
299
|
+
// ── ② 转录面:**封存**这一段的定稿正文,`answerSegment` 清空给下一段用 ──────────────────
|
|
300
|
+
// 🔴 清空是轮一 finding② 的修复本体:不清空的话,下一段的 delta 会接着往同一个缓冲里加,
|
|
301
|
+
// 然后被下一帧的 `content` 整段覆盖 —— 上一段的正文凭空消失(anthropic 车道逐块发
|
|
302
|
+
// `text_end`,这条时序是常态)。封存**不切消息**:段提交本体交的是 `sealed + segment`。
|
|
303
|
+
// 🔴 `committedPrefixDiverged` 那一形**一个字节都不交**(统一规则,见接口注):已提交的
|
|
304
|
+
// 那截撤不回,而切片拼不出 `content` —— 再补一条就是同一段话上屏两遍;该段唯一算数的
|
|
305
|
+
// 那一份在帧上,由宿主按 `committedPrefixLen` 指的那一截换掉(单位见回执头注)。
|
|
306
|
+
if (committedPrefixDiverged) {
|
|
307
|
+
// 🔴 **同步终答补差账 —— 但只在「整截都在当前这条消息里」那一形**(轮三 finding② 采纳,
|
|
308
|
+
// 轮四 finding①② 收窄)。这一形下本包不交正文,而屏上那一段在宿主照契约换过之后**就是**
|
|
309
|
+
// `content`;补差账若还留着旧的那一份,`result` 臂会把权威全文当成「屏上缺的后缀」
|
|
310
|
+
// 再渲一遍(第④臂),正好违反三处文案承诺的「不再交正文」。
|
|
311
|
+
//
|
|
312
|
+
// 🔴 **为什么只同步这一形**(轮四两条 finding 的根因,记明免得下一棒又去「补全」它):
|
|
313
|
+
// 段跨过包侧边界(工具卡 / 消息划界)时,这一截的字节落在**别的** assistant 消息里,而
|
|
314
|
+
// `committedText` / `previousCommittedText` 是**按消息**记的两本账 —— 把 `content` 往当前
|
|
315
|
+
// 这本账上写,就等于把上一条消息的正文算进当前这条:轮四实测 ① 迟到边界那一形会让随后
|
|
316
|
+
// 正常的 B 段被误报 `result_text_diverged`、甚至把该补的后缀压掉;② 同段跨两张卡时
|
|
317
|
+
// `previousCommittedText` 只留得下最后一条,守卫直接不成立、同步静默跳过。
|
|
318
|
+
// ⇒ 跨消息那几形**不碰账**。代价如实认领:那里的补差基线与「宿主换过之后的屏」对不齐,
|
|
319
|
+
// `result` 可能落第④臂补一截或落第⑤臂报一行 `result_text_diverged` —— 而这两者
|
|
320
|
+
// **都是本批之前就有的行为**(主树 0.68.0 上同一条时序实测同形:卡后有散文时
|
|
321
|
+
// `lastCommittedAnswerText` 取的就是卡**之后**那一条,终帧却是整条消息的全文)。
|
|
322
|
+
// 根因是「包按自己的边界切消息、引擎按 content block 切段」这两套边界对不齐,根治 =
|
|
323
|
+
// 「每个引擎段各自一条 committed 消息」(= CC 原生做法),那是分段行为改动,单独走。
|
|
324
|
+
if (committedText.endsWith(livePrefix)) {
|
|
325
|
+
committedText = committedText.slice(0, committedText.length - livePrefix.length) + content;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
segmentSealed += content.slice(committedPrefixLen);
|
|
330
|
+
}
|
|
331
|
+
answerSegment = '';
|
|
332
|
+
// ── ③ `answer`(D8 调试行的长度读位)跟着换尾 —— 只在尾巴真对得上时换 ──────────────────
|
|
333
|
+
// `appendAnswer`(durable 整条腿)会往同一个累加器里加字,那一形下本段已经不是它的后缀;
|
|
334
|
+
// 猜着改一个诊断计数器比让它原样更坏,所以对不上就不动(它只喂 `answerLen=` 那一行)。
|
|
335
|
+
if (answer.endsWith(liveSegment)) {
|
|
336
|
+
answer = answer.slice(0, answer.length - liveSegment.length) + content;
|
|
337
|
+
}
|
|
338
|
+
// 段锚缺席(整段已被 idle 提交光、或整段零 delta)才用本帧兜底 —— 在场**不覆盖**,
|
|
339
|
+
// 否则同一条流重放两次会拿到两个不同的 committed 消息 id(replay-id 不变量)。
|
|
340
|
+
if (segmentAnchor === null && anchor !== undefined)
|
|
341
|
+
segmentAnchor = anchor;
|
|
342
|
+
// ── ④ 段账收口:这一段由引擎明说已经写完 ⇒ 两本前缀账都归零、idle 配额重开 ──────────────
|
|
343
|
+
segmentCommitted = '';
|
|
344
|
+
previousSegmentCommitted = '';
|
|
345
|
+
idleFlushUsedInSegment = false;
|
|
346
|
+
return { diverged, committedPrefixLen, committedPrefixDiverged };
|
|
347
|
+
},
|
|
191
348
|
hasPendingContent: () => thinking.length > 0 ||
|
|
349
|
+
// L-310:已封存的定稿段同样是「有已生成未提交的内容」(不算进来会让它一直押到 turn 收口)。
|
|
350
|
+
segmentSealed.length > 0 ||
|
|
192
351
|
answerSegment.length > 0 ||
|
|
193
352
|
thinkingPending.length > 0 ||
|
|
194
353
|
textPending.length > 0,
|
|
@@ -211,6 +370,10 @@ export function createTextStream(ctx, idOf) {
|
|
|
211
370
|
if (committedText.length > 0)
|
|
212
371
|
previousCommittedText = committedText;
|
|
213
372
|
committedText = '';
|
|
373
|
+
// L-310:新消息开始 = 旧段的已提交前缀与**新**段无关 ⇒ 本段账归零;同 takeAnswerSegment,
|
|
374
|
+
// 归零前记进 `previousSegmentCommitted`(迟到的 `text_end` 还要拿它算分歧)。
|
|
375
|
+
previousSegmentCommitted += segmentCommitted;
|
|
376
|
+
segmentCommitted = '';
|
|
214
377
|
},
|
|
215
378
|
get committedAnswerText() {
|
|
216
379
|
return committedText;
|
|
@@ -27,6 +27,20 @@ export interface TurnFlags {
|
|
|
27
27
|
onFrameBeforeDispatch(m: Frame): Generator<AdapterOutput>;
|
|
28
28
|
/** A6 `turn_usage`:发 end 度量 + 压下一轮基线。 */
|
|
29
29
|
emitTurnUsageEnd(outputTokens: number): Generator<AdapterOutput>;
|
|
30
|
+
/**
|
|
31
|
+
* A6 `turn_usage` 的**无数字那一支**(0.68.0;异源对抗复审轮五实抓)——
|
|
32
|
+
* **只压下一轮基线,不发任何帧**。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 为什么必须把它拆出来:`emitTurnUsageEnd` 一直在做**两件事** —— 发 end 度量 **与** 把
|
|
35
|
+
* `messageStartEmitted` 复位(下一个 model round 重新压 TTFT 基线)。而 #711 之后,一轮没量出账时
|
|
36
|
+
* 这一拍**没有数字可发**(占位零不许当读数,见 `adapter/runStream.ts` 的那条判据)⇒ 若整条跳过
|
|
37
|
+
* `emitTurnUsageEnd`,**连模型轮边界也一起跳过了**:下一轮不再发 `response_metrics{start}`,
|
|
38
|
+
* 它的用量于是按**上一轮**的基线对账(实测:首轮 400 字符缺账、次轮 4 字符 + 50 tokens,
|
|
39
|
+
* 计数从 150 掉到 101,且下一轮的 TTFT 记录一并丢失)。
|
|
40
|
+
* 🔴 判据分家:**「这一轮结束了」是一件事,「这一轮花了多少」是另一件事** —— 前者与有没有数字
|
|
41
|
+
* 无关,后者才受占位零那条判据管。合成一个动作,过滤数字就会顺手把边界也过滤掉。
|
|
42
|
+
*/
|
|
43
|
+
noteModelRoundBoundary(): void;
|
|
30
44
|
/** A7 `result`:只有还没发过 end 且这帧真带了 outputTokens 时才补发(`!endEmitted` 门)。 */
|
|
31
45
|
/** `readOutputTokens` 惰性:只在真的要发 end 度量时才读 usage(与拆分前「`!endEmitted`
|
|
32
46
|
* 门内才读 `m.usage`」逐字同语义 —— 对抗复审 #3 收货修)。 */
|
package/dist/adapt/turnFlags.js
CHANGED
|
@@ -44,7 +44,10 @@ export function createTurnFlags(ctx, turnStartAt) {
|
|
|
44
44
|
*emitTurnUsageEnd(outputTokens) {
|
|
45
45
|
yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens });
|
|
46
46
|
endEmitted = true;
|
|
47
|
-
messageStartEmitted = false; // 下一 model round 重新压基线
|
|
47
|
+
messageStartEmitted = false; // 下一 model round 重新压基线(与 noteModelRoundBoundary 同一件事)
|
|
48
|
+
},
|
|
49
|
+
noteModelRoundBoundary: () => {
|
|
50
|
+
messageStartEmitted = false;
|
|
48
51
|
},
|
|
49
52
|
*emitResultEndIfNeeded(readOutputTokens) {
|
|
50
53
|
if (endEmitted)
|
package/dist/adapt.js
CHANGED
|
@@ -38,7 +38,10 @@ export const ADAPTER_COVERAGE = {
|
|
|
38
38
|
'message_committed',
|
|
39
39
|
// #318 件①(2026-08-21):`engine_notice` 引擎结构化通告 —— 投 chrome(开集,一个码都不判)。
|
|
40
40
|
'engine_notice',
|
|
41
|
-
// #323 / core #447:assistant 散文段边界 →
|
|
41
|
+
// #323 / core #447 + L-310(0.68.1 改口):assistant 散文段边界 → **整段替换文本缓冲** +
|
|
42
|
+
// chrome text_segment_end(子流断闸在替换之前)。修前这里写「本批**不**动文本缓冲」——
|
|
43
|
+
// server 7.75.3 起 `text_end.content` 经脱敏器而 `text_delta` 仍逐字,不动缓冲 = 未脱敏字节
|
|
44
|
+
// committed 进本地转录;替换语义见 `adapt/textStream.ts` 的 `replaceAnswerSegment` 头注。
|
|
42
45
|
'text_end',
|
|
43
46
|
'prompt_suggestions',
|
|
44
47
|
'retry_status',
|
|
@@ -85,11 +85,33 @@ export declare const CLAIM_HELD_STATES: readonly string[];
|
|
|
85
85
|
* 判成「无公共属性」而拒绝赋值)。三处 `unknown` 是边界职责形(本包不拥有 run 记录的类型,
|
|
86
86
|
* 消费点 `readStatus` 就是窄化动作本身;typeshape 门 RATCHET 逐条登记)。
|
|
87
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* `GET /v1/runs/:id` 的行,**只到本层读得到的那几格**(0.68.0 / L-230)。
|
|
90
|
+
*
|
|
91
|
+
* 🔴 具名而不是内联匿名形:三格都是 wire 值(`unknown`),而端要按它们分支 —— 一个有名字的形让
|
|
92
|
+
* 宿主的适配层能声明自己返回什么,也让 typeshape 门的 B4 棘轮不为这一处升一格。
|
|
93
|
+
* 🔴 **三格全可选**:老引擎只发 `status`;`heldBy` / `cancelRequested` 是 server 7.73.0 S-122 P-45
|
|
94
|
+
* 的 additive 读面,缺席一律走既有路(见 `readClaimHolder` / `readCancelRequested` 的三态)。
|
|
95
|
+
* 🔴 行上其余键本层**一个都不读**(它们仍在 wire 上,只是这一层不需要);要读的端自己窄化。
|
|
96
|
+
*/
|
|
97
|
+
export interface DurableRunRecordView {
|
|
98
|
+
/** run 行状态词(开集;本层按 {@link CLAIM_RELEASED_STATES} / {@link CLAIM_HELD_STATES} 白名单判)。 */
|
|
99
|
+
readonly status?: unknown;
|
|
100
|
+
/** 谁占着这个会话:非空串 = 那条 run 还占着;**`null` = 一句正面事实(交出来了)**;整键缺席 = 老引擎。 */
|
|
101
|
+
readonly heldBy?: unknown;
|
|
102
|
+
/** 这条 run 上**有没有受理过一次取消请求**(不是「已取消」)。整键缺席 = 老引擎不发。 */
|
|
103
|
+
readonly cancelRequested?: unknown;
|
|
104
|
+
}
|
|
88
105
|
export interface DurableRunVerbs {
|
|
89
106
|
events: unknown;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
/**
|
|
108
|
+
* `GET /v1/runs/:id`。
|
|
109
|
+
* 🔴 0.68.0(L-230;server 7.73.0 S-122 P-45 读面):型面补两个**可选**位 —— `heldBy` 与
|
|
110
|
+
* `cancelRequested`。两者都是 **additive**:老引擎不发,读点按缺席走既有路
|
|
111
|
+
* ({@link CLAIM_RELEASED_STATES}),所以拓型不是「要求宿主升级」,是「宿主已经透传的那两位
|
|
112
|
+
* 从此有读点」(L-230 ⑤ 逐字:缺的是读点不是通道)。
|
|
113
|
+
*/
|
|
114
|
+
get?: (taskId: string, opts?: DurableRunCallOpts) => Promise<DurableRunRecordView | null>;
|
|
93
115
|
cancel?: (taskId: string, opts?: DurableRunCallOpts) => Promise<unknown>;
|
|
94
116
|
/** 三选卡的 steer 腿(`POST /v1/runs/:id/steer`)。🔴 **AT-MOST-ONCE**:steer 非幂等,契约逐字
|
|
95
117
|
* 「it is NEVER retried」(本包 `controlRouter` 同律)—— 本层调它恰一次,失败如实上屏由人重发。
|
|
@@ -396,9 +418,11 @@ export type SelfHealOutcome =
|
|
|
396
418
|
taskId: string;
|
|
397
419
|
waitedMs: number;
|
|
398
420
|
aborted: boolean;
|
|
399
|
-
/** 🔴
|
|
400
|
-
*
|
|
401
|
-
*
|
|
421
|
+
/** 🔴 最近一次探测**拿到了持锁的正面证据**吗。判据**两条取并**(0.68.0 / L-230):
|
|
422
|
+
* ① 引擎直说的 `heldBy`(非空串 = 有一条 run 的名字在锁上);② 回落到状态词白名单
|
|
423
|
+
* ({@link CLAIM_HELD_STATES},老引擎不发 `heldBy` 时的既有路)。**不是**「lastStatus 非空」——
|
|
424
|
+
* 表外的新状态词非空却证明不了持锁。false = 两条都读不出 / 读到一个不认识的词 ⇒ 文案只许说
|
|
425
|
+
* 「确认不了」,绝不说「它还占着」。 */
|
|
402
426
|
confirmedHeld: boolean;
|
|
403
427
|
}
|
|
404
428
|
/**
|
|
@@ -489,6 +513,16 @@ export type SelfHealSubmissionDisposition = 'held-for-decision' | 'resending' |
|
|
|
489
513
|
* 「已解锁」证不出,自动重投可能撞回仍锁着的会话。
|
|
490
514
|
*/
|
|
491
515
|
export declare function selfHealSubmissionDisposition(outcome: SelfHealOutcome): SelfHealSubmissionDisposition;
|
|
516
|
+
/**
|
|
517
|
+
* cancel 回执(202 体)/ run 行上的 `cancelRequested`(0.68.0 / L-230;server 7.73.0 S-122 P-45)。
|
|
518
|
+
*
|
|
519
|
+
* 🔴 **三态**同上:`true` / `false` 都是真读数,**读不出 ⇒ `undefined`**(老引擎不发这一位)。
|
|
520
|
+
* 绝不折 `false` —— 「没请求过取消」与「不知道有没有请求过」对下一步的含义不同:前者可以放心
|
|
521
|
+
* 再发一次 cancel,后者再发就可能是第二枪。
|
|
522
|
+
* 🔴 它**不是**「已经取消了」:上游逐字是 *requested*,一次**已受理的请求**,不是终局。
|
|
523
|
+
* 会话有没有交出来仍然只由 {@link readClaimHolder} 与 {@link CLAIM_RELEASED_STATES} 回答。
|
|
524
|
+
*/
|
|
525
|
+
export declare function readCancelRequested(body: unknown): boolean | undefined;
|
|
492
526
|
/**
|
|
493
527
|
* 一次**至多一次**(non-idempotent)POST 失败之后:到底是「服务端明确拒了」还是「不知道有没有
|
|
494
528
|
* 落地」。steer 与 cancel 两条腿共用这一把尺 —— 它们同属「这一枪不能盲发第二次」的族,而两类
|
|
@@ -534,6 +568,19 @@ export interface ClaimReleaseVerdict {
|
|
|
534
568
|
* 已经不知道的事,恰好塌回本批要修的那一格。
|
|
535
569
|
*/
|
|
536
570
|
lastStatus: string | null;
|
|
571
|
+
/**
|
|
572
|
+
* **最近一次**探测读到的 `heldBy` 三态(0.68.0 / L-230;见 `readClaimHolder`)。
|
|
573
|
+
*
|
|
574
|
+
* · `'held'` —— 引擎**直说**那条 run 还占着这个会话(比按状态词推断强得多的证据);
|
|
575
|
+
* · `'released'` —— 引擎**直说**已经交出来了;
|
|
576
|
+
* · `'unknown'` —— 这一位读不出(老引擎不发 / 本发没有回答)。
|
|
577
|
+
*
|
|
578
|
+
* 🔴 与 `lastStatus` 同律:**只记最近那一次有回答的探测**;截止掐断(连回答都没有)不改写它。
|
|
579
|
+
* 🔴 文案层的用法:`'held'` 是「那条 run 还占着这个会话」这句话的**直接证据**,比
|
|
580
|
+
* {@link CLAIM_HELD_STATES} 那条按词推断的路更强;两条都读不出时收口成「确认不了」,
|
|
581
|
+
* 绝不替引擎下一个证不出的断言。
|
|
582
|
+
*/
|
|
583
|
+
lastHolder: 'held' | 'released' | 'unknown';
|
|
537
584
|
}
|
|
538
585
|
/** {@link waitForClaimRelease} 的注入口(时钟/等待/中止/窗口全部可注入 —— 门要能在零墙钟下判)。 */
|
|
539
586
|
export interface ClaimReleaseWaitDeps {
|