@sema-agent/client-core 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +241 -0
- package/README.md +1 -1
- package/dist/adapt/arms.js +3 -0
- package/dist/adapter/activeRunSelfHeal.d.ts +35 -2
- package/dist/adapter/activeRunSelfHeal.js +157 -4
- package/dist/adapter/downstream/eventToSdkMessage.js +5 -0
- package/dist/adapter/runStream.d.ts +63 -1
- package/dist/adapter/runStream.js +38 -0
- package/dist/engineCapsCache.d.ts +48 -2
- package/dist/engineCapsCache.js +157 -11
- package/dist/fleet/fleetProjection.d.ts +9 -1
- package/dist/fleet/fleetProjection.js +13 -3
- package/dist/fleetTaskDesc.d.ts +5 -1
- package/dist/fleetTaskDesc.js +39 -2
- package/dist/hitl/armedGateRegistry.js +11 -3
- package/dist/hitl/hitlBridge.d.ts +20 -0
- package/dist/hitl/hitlBridge.js +156 -12
- package/dist/hitl/parkResolver.d.ts +1 -0
- package/dist/hitl/parkResolver.js +22 -2
- package/dist/hitl/toolApprovalWire.d.ts +88 -7
- package/dist/hitl/toolApprovalWire.js +50 -6
- package/dist/request/taskRequest.js +11 -11
- package/dist/retryStatus.d.ts +38 -3
- package/dist/retryStatus.js +15 -5
- package/dist/toolResult.d.ts +8 -0
- package/dist/toolResult.js +15 -0
- package/docs/INTEGRATION-CLIENTS.md +20 -19
- package/package.json +2 -2
|
@@ -50,7 +50,56 @@ export interface ActiveRunPendingGate {
|
|
|
50
50
|
* 证据」。消费方只做**在场才说**的加法:纯归因一句,不参与任何分诊/动作。
|
|
51
51
|
*/
|
|
52
52
|
readonly governanceForced?: true;
|
|
53
|
+
/**
|
|
54
|
+
* 这道 park 门的**非秘密稳定身份**(#285 件2,0.36.0;server ≥7.33.0 `PendingGateMaterial.checkpointId`
|
|
55
|
+
* / core 5.42.0 `Checkpoint.checkpointId`,`mintCheckpointId` 铸 = `cp_`+uuid)。逐字过境,本层零加工。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 **它补的是一个真实的空洞**:park 门此前在 wire 上**没有任何稳定身份** —— 唯一单射的键是
|
|
58
|
+
* `token`,而 token 是秘密能力、永不上 wire,于是消费端只能拿 `(sessionId, kind)` 这种非单射组合
|
|
59
|
+
* 去猜「这次的门」和「上次的门」是不是同一道。本键是为此存在的非秘密孪生身份(identification vs
|
|
60
|
+
* capability,两条独立的轴;**独立铸造,不是 token 摘要**)。
|
|
61
|
+
*
|
|
62
|
+
* 🔴 **缺席是三成因合流的一个形**(server 逐条成文,别只记第一条):① 该键诞生前 park 的 legacy 行;
|
|
63
|
+
* ② 行上有值但没过 server 的信任边界校验(空串/超长/**含 token**)⇒ 数据污染,而它在运维面上
|
|
64
|
+
* 与 ① **不可分辨**(server 自己登记的残留);③ 整只 `pendingGate` 读取失败(那时本键连位置都没有)。
|
|
65
|
+
* ⇒ 读作「**不知道这道门叫什么**」,**不是**「这道门没有身份」,更**不是**「这一定是条老行」。
|
|
66
|
+
*
|
|
67
|
+
* 🔴 **不是跨调用的稳定去重键**(逐条出自 server 侧 `PendingGateMaterial.checkpointId` 的顶注 ——
|
|
68
|
+
* 那段是上游自己的对抗复审验真后如实登记的,不是本包的推测):
|
|
69
|
+
* 同 session 可并存两条 pending checkpoint(表上**没有** per-session 唯一约束),
|
|
70
|
+
* 而 server 选行是**无序 `LIMIT 1`** ⇒ 同一情形连续两次 409 完全可能报出**不同**的 id(选中了另一行),
|
|
71
|
+
* 也可能 id 不变而 `activeTaskId` 已换。本材料的**每一格**(kind/decidePath/governanceForced/本键)
|
|
72
|
+
* 共享这条归属不确定性,本键是**继承**它、不是引入它。
|
|
73
|
+
* ⇒ 可安全用于:落日志/排障关联,以及 {@link pendingGateIsProvablyDifferent} 那个**单向**判断。
|
|
74
|
+
* **不可**用于:跨 turn 的卡去重键、「id 相同 ⇒ 同一道门」的反向推理。
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ **辖域如实**:上游本批只把这一位投到**这一个载体**(409 `conflict.session_active_run` 体 +
|
|
77
|
+
* 其 SSE done 帧)。另外三条 park 读面(`/v1/assistant/inbox`、`GET /v1/approvals`、
|
|
78
|
+
* `/v1/approvals/stream`)今天**都没有**这一格,且补它们的代价不同(后两条要给 checkpoint 表反范式
|
|
79
|
+
* 一列 = SQL 面双库门)。⇒ **别在消费码里预支「两面能对上」** —— 拿本键去 join durable 队列行必然
|
|
80
|
+
* 落空,而落空与 legacy 行同形。
|
|
81
|
+
*/
|
|
82
|
+
readonly checkpointId?: string;
|
|
53
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* `pendingGate` 的**单向**同一性判断(#285 件2,0.36.0):「我现在看到的这道门,能不能**证明**它
|
|
86
|
+
* 不是刚才那一道?」
|
|
87
|
+
*
|
|
88
|
+
* `true` ⇔ 两侧都带 {@link ActiveRunPendingGate.checkpointId} **且**两者不等。其余一律 `false`。
|
|
89
|
+
*
|
|
90
|
+
* 🔴 **`false` 的语义是「证不出」,不是「同一道门」** —— 这不是措辞洁癖,是上游写死的可用性边界:
|
|
91
|
+
* server 选行是无序 `LIMIT 1` 且同 session 可并存多条 pending 行,所以「id 相同」推不出「同一道门」
|
|
92
|
+
* (id 不变而 `activeTaskId` 已换是成文的可能形),「id 缺席」更只是「不知道它叫什么」(三成因合流)。
|
|
93
|
+
* 上游明确只背书**这一个方向**:「id 变了 ⇒ 我看到的确实不是刚才那一行」。
|
|
94
|
+
*
|
|
95
|
+
* 🔴 **为什么归包而不是让三端各写一遍**:这个谓词的价值全在**它拒绝回答的那一半**。三端各自手搓时,
|
|
96
|
+
* 「两个 id 相等就当同一张卡去重」是最自然、也最容易写出来的一行 —— 而它恰恰是上游明说买不到的那个
|
|
97
|
+
* 方向,写出来就是一条按错误前提去重的卡链。把可答的那半做成唯一入口,不可答的那半就没有顺手的写法。
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ 它**不做**任何 kind/decidePath 比较:那两位回答的是「哪种门/去哪决议」,与「是不是同一行」正交
|
|
100
|
+
* (同一行的 kind 不会变,不同行的 kind 完全可能相同)。要判种类变化请直接读 `kind`。
|
|
101
|
+
*/
|
|
102
|
+
export declare function pendingGateIsProvablyDifferent(prev: ActiveRunPendingGate | null | undefined, next: ActiveRunPendingGate | null | undefined): boolean;
|
|
54
103
|
/** #114/[C65] 单源化:三端共用的 409 active-run 拒收终帧判别(富信号形,包级唯一权威)。
|
|
55
104
|
* 🔧 2026-08-02 事实纠正([C77]③):原文写的是「与壳/desktop **各自**照抄件同义……各端换包
|
|
56
105
|
* 导入删本地抄件」,而 desktop 全树扫描(`activeRunBusySignal` / `session_active_run` /
|
|
@@ -62,10 +111,23 @@ export interface ActiveRunPendingGate {
|
|
|
62
111
|
export interface ActiveRunBusySignal {
|
|
63
112
|
/** 占锁 run 的 id;wire 没带 ⇒ null(诚实缺席,绝不铸造)。 */
|
|
64
113
|
readonly activeTaskId: string | null;
|
|
65
|
-
/** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。
|
|
114
|
+
/** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。
|
|
115
|
+
* 🔴 [4664] 成文的可信度边界(2026-08-19,四个洞逐条在案):park 态不在 reapStale 射程 /
|
|
116
|
+
* `updated_at` 误杀窗 / 409 与 poll 口径短暂相左 / LOCAL(file)车道无周期自愈腿 ——
|
|
117
|
+
* **别把这一位的 `running` 当绝对真值**;SQL 车道死 run 的自愈上界 ≈180s(心跳 30s +
|
|
118
|
+
* REAP_RUN_STALE_SEC 120 + tick 60),LOCAL 车道**无上界**。消费方出卡前的存活对账腿见
|
|
119
|
+
* `activeRunSelfHeal.runningChoiceArm`。 */
|
|
66
120
|
readonly activeTaskStatus: string | null;
|
|
67
121
|
/** 409 body 的 pendingGate(wire 给的真路由,不自造);缺席 ⇒ null。 */
|
|
68
122
|
readonly pendingGate: ActiveRunPendingGate | null;
|
|
123
|
+
/**
|
|
124
|
+
* 409 体上的占锁 run **最近活动距今毫秒数**(Inkglow-1085 P0b,[4664]:这一格是 409 体内唯一
|
|
125
|
+
* 可消费的活性证据位)。在场 = 有限非负数;缺席 = 老 server / 坏形(两者同形,不猜)——
|
|
126
|
+
* **缺席 ≠「没有活动」**。展示判别位:端据此渲「该 run 已 N 分钟无活动」类提示;
|
|
127
|
+
* 不参与本包的分诊动作(存活对账腿的判据仍是 runs.get 读回的状态词)。
|
|
128
|
+
* ⚠️ 结构视图读(SDK 7.2.0 类型面尚无此键)—— probeCause 同款「领先锚」姿势,锚补上跟批。
|
|
129
|
+
*/
|
|
130
|
+
readonly msSinceLastActivity?: number;
|
|
69
131
|
}
|
|
70
132
|
/**
|
|
71
133
|
* 判别一个**原始 AgentEvent** 是不是 409 active-run 拒收终帧;非 busy ⇒ null。
|
|
@@ -10,6 +10,32 @@ import { coerceOutput, publishSubagentContentEvent } from '../subagentContentSto
|
|
|
10
10
|
import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from '../engineErrorCodes.js';
|
|
11
11
|
/** 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。 */
|
|
12
12
|
const MAIN = { lane: 'main' };
|
|
13
|
+
/**
|
|
14
|
+
* `pendingGate` 的**单向**同一性判断(#285 件2,0.36.0):「我现在看到的这道门,能不能**证明**它
|
|
15
|
+
* 不是刚才那一道?」
|
|
16
|
+
*
|
|
17
|
+
* `true` ⇔ 两侧都带 {@link ActiveRunPendingGate.checkpointId} **且**两者不等。其余一律 `false`。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 **`false` 的语义是「证不出」,不是「同一道门」** —— 这不是措辞洁癖,是上游写死的可用性边界:
|
|
20
|
+
* server 选行是无序 `LIMIT 1` 且同 session 可并存多条 pending 行,所以「id 相同」推不出「同一道门」
|
|
21
|
+
* (id 不变而 `activeTaskId` 已换是成文的可能形),「id 缺席」更只是「不知道它叫什么」(三成因合流)。
|
|
22
|
+
* 上游明确只背书**这一个方向**:「id 变了 ⇒ 我看到的确实不是刚才那一行」。
|
|
23
|
+
*
|
|
24
|
+
* 🔴 **为什么归包而不是让三端各写一遍**:这个谓词的价值全在**它拒绝回答的那一半**。三端各自手搓时,
|
|
25
|
+
* 「两个 id 相等就当同一张卡去重」是最自然、也最容易写出来的一行 —— 而它恰恰是上游明说买不到的那个
|
|
26
|
+
* 方向,写出来就是一条按错误前提去重的卡链。把可答的那半做成唯一入口,不可答的那半就没有顺手的写法。
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ 它**不做**任何 kind/decidePath 比较:那两位回答的是「哪种门/去哪决议」,与「是不是同一行」正交
|
|
29
|
+
* (同一行的 kind 不会变,不同行的 kind 完全可能相同)。要判种类变化请直接读 `kind`。
|
|
30
|
+
*/
|
|
31
|
+
export function pendingGateIsProvablyDifferent(prev, next) {
|
|
32
|
+
const a = prev?.checkpointId;
|
|
33
|
+
const b = next?.checkpointId;
|
|
34
|
+
// 任一侧缺席 ⇒ 证不出(缺席 = 不知道这道门叫什么,三成因合流;绝不当成「不同」)。
|
|
35
|
+
if (typeof a !== 'string' || typeof b !== 'string')
|
|
36
|
+
return false;
|
|
37
|
+
return a !== b;
|
|
38
|
+
}
|
|
13
39
|
/**
|
|
14
40
|
* 判别一个**原始 AgentEvent** 是不是 409 active-run 拒收终帧;非 busy ⇒ null。
|
|
15
41
|
* 判据=结构两腿(canonical `errorCode` 优先 → `activeTaskId` 在场);人话文案腿已随 #117 提货
|
|
@@ -51,16 +77,28 @@ export function activeRunBusySignal(ev) {
|
|
|
51
77
|
// 出身位(A-028.1):**只认严格 true**。任何别的形(false / 'true' / 1 / 缺席)一律不置键 ——
|
|
52
78
|
// 键在场即渲徽标类文案,把一个含糊值读成「治理强制」= 对用户下一个证不出的断言。
|
|
53
79
|
const g = rawGate.governanceForced;
|
|
80
|
+
// 身份位(#285 件2):**非空串才置键**,坏形一律降缺席 —— 与 kind/decidePath 同族口径。
|
|
81
|
+
// 🔴 **刻意不做 `cp_`+UUID 格式正则、也不叠长度上限**,与 server 铸点同裁:上游亲跑验过,严格
|
|
82
|
+
// 格式钉**买不到安全性**(32 位 hex token 按 uuid 分组后逐字满足该正则,剥掉装饰就是完整能力键),
|
|
83
|
+
// 只额外买来「core 换形我方静默丢键」的代价;长度域已在 server 投影处夹取,客户端再叠一道
|
|
84
|
+
// **更窄**的域 = 上游放宽当天我方静默丢键,而丢键与 legacy 行在运维面上不可分辨。
|
|
85
|
+
// 窄读域只许等于或宽于铸点域([4050] 随批立的纪律)。
|
|
86
|
+
const cp = rawGate.checkpointId;
|
|
54
87
|
pendingGate = {
|
|
55
88
|
kind: typeof k === 'string' && k.length > 0 ? k : null,
|
|
56
89
|
decidePath: typeof d === 'string' && d.length > 0 ? d : null,
|
|
57
90
|
...(g === true ? { governanceForced: true } : {}),
|
|
91
|
+
...(typeof cp === 'string' && cp.length > 0 ? { checkpointId: cp } : {}),
|
|
58
92
|
};
|
|
59
93
|
}
|
|
94
|
+
// [4664] 活性证据位:只认**有限非负数**(NaN/Infinity/负数/串都是坏形 ⇒ 键不 stamp —— 一个
|
|
95
|
+
// 编造的「N 分钟无活动」比没有更坏)。
|
|
96
|
+
const msIdle = carrier.msSinceLastActivity;
|
|
60
97
|
return {
|
|
61
98
|
activeTaskId: cls.handle,
|
|
62
99
|
activeTaskStatus: typeof status === 'string' && status.length > 0 ? status : null,
|
|
63
100
|
pendingGate,
|
|
101
|
+
...(typeof msIdle === 'number' && Number.isFinite(msIdle) && msIdle >= 0 ? { msSinceLastActivity: msIdle } : {}),
|
|
64
102
|
};
|
|
65
103
|
}
|
|
66
104
|
/**
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
* · kick 幂等(in-flight 去重),绝不 throw;
|
|
13
13
|
* · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
|
|
14
14
|
* · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
|
|
15
|
-
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL
|
|
16
|
-
*
|
|
15
|
+
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **失效口的由来**(#307 双扫 S25 勘误,2026-08-19)。本段此前自述「随每次
|
|
18
|
+
* `createLiveConversationClient` 构造重 kick(引擎温切重启后新构造自然重探)」——**那句话不成立**:
|
|
19
|
+
* {@link kickEngineCapsProbe} 首行就是 `capsByBase.has(baseUrl) ⇒ return`,而引擎温切
|
|
20
|
+
* (respawn / restartEngine)重启后 baseUrl 常与重启前**一模一样**,于是「新构造」被这条幂等闸
|
|
21
|
+
* 原样挡住,缓存里留的永远是**旧引擎**那一版的 caps。后果不是报错,是安静地按旧能力位走:
|
|
22
|
+
* 新引擎新增的车道被判成「没有」(藏功能),旧引擎有而新引擎撤掉的车道被判成「有」(走死路)。
|
|
23
|
+
* 此前除测试钩 {@link __resetEngineCapsCacheForTests} 外**没有任何生产失效路径**。
|
|
24
|
+
* 修 = 显式失效口 {@link invalidateEngineCaps},由知道「引擎换人了」的那一层(壳的 respawn /
|
|
25
|
+
* restartEngine)在重启后调用 —— 缓存自己无从分辨「同一个 baseUrl 后面还是不是同一个引擎」,
|
|
26
|
+
* 猜(TTL / 每次构造清)只会把一个确定事实换成一个定时器。
|
|
17
27
|
*/
|
|
18
28
|
/** 构造期 kick(async 幂等);probe = client.capabilities 薄闭包。 */
|
|
19
29
|
export declare function kickEngineCapsProbe(baseUrl: string, probe: () => Promise<unknown>): void;
|
|
@@ -35,5 +45,41 @@ export declare function engineCapTrue(baseUrl: string | undefined, key: string):
|
|
|
35
45
|
* 引擎能力位缺席时任何「假定它有」的分支都是对用户/模型的谎报。
|
|
36
46
|
*/
|
|
37
47
|
export declare function engineCapString(baseUrl: string | undefined, key: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* **生产失效口**(#307 双扫 S25,client-core 0.37.0):把该 base 的探测结果作废,
|
|
50
|
+
* 使**下一次** {@link kickEngineCapsProbe} 真正重探(而不是被幂等闸原样挡回)。
|
|
51
|
+
*
|
|
52
|
+
* 消费方 = **知道「这个 baseUrl 后面换了一个引擎进程」的那一层**,目前唯一一处是壳的引擎温切:
|
|
53
|
+
* cli `respawn` / `restartEngine` 成功后、重新构造 `createLiveConversationClient` **之前**调用。
|
|
54
|
+
* 库这一层看到的只有一个字符串 base,分辨不出对面是不是同一个进程,所以失效必须由上面显式下达 ——
|
|
55
|
+
* 见模块头注:靠 TTL 或「每次构造清」去猜,是把一个确定事实换成一个定时器。
|
|
56
|
+
*
|
|
57
|
+
* 语义与边界:
|
|
58
|
+
* · **推进代际**({@link genByBase}) + 清 `capsByBase`(已判结果) + 清 `inFlight`(在途去重位)。
|
|
59
|
+
* · **不 abort** 在途探测(没有可 abort 的把手,probe 是调用方给的薄闭包),改用代际让它
|
|
60
|
+
* **安静退场**:被顶掉的旧 run 落地后既不写缓存、也不归还任何位。所以本口在**探测在途时
|
|
61
|
+
* 调用是安全的** —— 旧引擎那次响应绝不会覆盖新引擎的能力位(2026-08-19 对抗复审 [high])。
|
|
62
|
+
* · `settleByBase` **不清**:{@link engineCapsSettled} 的语义是「等**当前这一次**探测落地」,
|
|
63
|
+
* 把在途 promise 抽走会让正在 await 的调用方立即拿到 resolve(假「已落地」)。旧 run 的
|
|
64
|
+
* settle 位在被新 run 顶掉后由代际认领保护,旧 run 的 `finally` 不会误删。
|
|
65
|
+
* · 空串 ⇒ no-op。**从没探过也从没失效过的 base ⇒ 真 no-op**(不留代际条目):那种 base 上
|
|
66
|
+
* 既无判定也无在途 run,没有任何陈旧写入可挡,留条目只会让任意串撑大表。
|
|
67
|
+
* · 绝不 throw(与本模块其余口同款:失效口在错误路径上响 = 把一个清理动作变成新的故障源)。
|
|
68
|
+
*
|
|
69
|
+
* 🔴 **推荐用两参形 `invalidateEngineCaps(baseUrl, probe)`**(对抗复审第三轮 [medium] 采纳,
|
|
70
|
+
* 2026-08-19)。单参形与「下一次 kick」之间有一个**真窗**:失效之后、新探测注册之前,如果旧代际
|
|
71
|
+
* 探测正好在这一拍落地,`settleByBase` 里已经没有更新代际的条目 ⇒ {@link engineCapsSettled} 把
|
|
72
|
+
* 等待者放走,而缓存刚被清空 ⇒ 等待者把**当代引擎的能力位读成缺席**。窗口只在调用方于失效与 kick
|
|
73
|
+
* 之间 `await` 了什么时才张开(同步块里 JS 单线程,旧探测的续体根本插不进来),但引擎温切本身就是
|
|
74
|
+
* 异步流程,所以它是可达的。
|
|
75
|
+
* 两参形把「推进代际」与「注册替代探测」放进**同一个同步块** ⇒ 窗口按构造不存在,等待者会被接力
|
|
76
|
+
* 到新探测上(见 {@link engineCapsSettled} 的跨代际接力)。壳的 respawn/restartEngine 应当用它。
|
|
77
|
+
* 单参形保留给「只想丢掉缓存、这一刻没有替代探测」的调用方 —— 那种情形下等待者读到**未判**
|
|
78
|
+
* 是诚实结局(判据永远是缓存位),不是缺陷:硬等一个可能永远不会来的 kick 才是。
|
|
79
|
+
*
|
|
80
|
+
* @param probe 可选的**替代探测**(与新引擎同一拍注册)。给了就等价于「失效 + 立刻 kick」,
|
|
81
|
+
* 且中间没有任何可插入点。
|
|
82
|
+
*/
|
|
83
|
+
export declare function invalidateEngineCaps(baseUrl: string | undefined, probe?: () => Promise<unknown>): void;
|
|
38
84
|
/** 测试钩子。 */
|
|
39
85
|
export declare function __resetEngineCapsCacheForTests(): void;
|
package/dist/engineCapsCache.js
CHANGED
|
@@ -12,21 +12,77 @@
|
|
|
12
12
|
* · kick 幂等(in-flight 去重),绝不 throw;
|
|
13
13
|
* · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
|
|
14
14
|
* · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
|
|
15
|
-
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL
|
|
16
|
-
*
|
|
15
|
+
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **失效口的由来**(#307 双扫 S25 勘误,2026-08-19)。本段此前自述「随每次
|
|
18
|
+
* `createLiveConversationClient` 构造重 kick(引擎温切重启后新构造自然重探)」——**那句话不成立**:
|
|
19
|
+
* {@link kickEngineCapsProbe} 首行就是 `capsByBase.has(baseUrl) ⇒ return`,而引擎温切
|
|
20
|
+
* (respawn / restartEngine)重启后 baseUrl 常与重启前**一模一样**,于是「新构造」被这条幂等闸
|
|
21
|
+
* 原样挡住,缓存里留的永远是**旧引擎**那一版的 caps。后果不是报错,是安静地按旧能力位走:
|
|
22
|
+
* 新引擎新增的车道被判成「没有」(藏功能),旧引擎有而新引擎撤掉的车道被判成「有」(走死路)。
|
|
23
|
+
* 此前除测试钩 {@link __resetEngineCapsCacheForTests} 外**没有任何生产失效路径**。
|
|
24
|
+
* 修 = 显式失效口 {@link invalidateEngineCaps},由知道「引擎换人了」的那一层(壳的 respawn /
|
|
25
|
+
* restartEngine)在重启后调用 —— 缓存自己无从分辨「同一个 baseUrl 后面还是不是同一个引擎」,
|
|
26
|
+
* 猜(TTL / 每次构造清)只会把一个确定事实换成一个定时器。
|
|
17
27
|
*/
|
|
18
28
|
const capsByBase = new Map();
|
|
19
29
|
const inFlight = new Set();
|
|
20
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* in-flight 探测的 settle 载体(**带代际**;finally 只删自己那一条,engineCapsSettled 消费)。
|
|
32
|
+
*
|
|
33
|
+
* `superseded` = **代际变更信号**(对抗复审第四轮 [high] 采纳,2026-08-19):
|
|
34
|
+
* {@link invalidateEngineCaps} 推进代际时 resolve 它,把等在**旧代际**上的调用方叫醒去重新求值,
|
|
35
|
+
* 而不是让它们被吊在一个可能永不落地的旧探测上。叫醒**不等于放行** —— 醒来后仍走
|
|
36
|
+
* {@link engineCapsSettled} 的循环:表里换上了新代际就接着等新探测,没换就按诚实缺席返回。
|
|
37
|
+
*/
|
|
21
38
|
const settleByBase = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* per-base **代际计数**(#307 S25 对抗复审 [high] 采纳,2026-08-19)。
|
|
41
|
+
*
|
|
42
|
+
* 为什么必须有:{@link invalidateEngineCaps} 清 `inFlight` 之后,同 base 立刻可以再 kick,于是
|
|
43
|
+
* **两次探测并发跑在同一份无版本共享态上**。没有代际时的两个真后果(都发生在「重启撞上一次慢
|
|
44
|
+
* capabilities 请求」这个恰恰最该正确的时刻):
|
|
45
|
+
* · 旧引擎那次探测**后**落地 ⇒ 它把 `capsByBase` 覆盖回**旧引擎**的能力位,失效等于没做;
|
|
46
|
+
* · 旧探测的 `finally` 删掉的是**新 run** 的 `inFlight`/`settleByBase` 条目 ⇒
|
|
47
|
+
* {@link engineCapsSettled} 提前 resolve(假「已落地」)+ 幂等闸被打开(重复探测)。
|
|
48
|
+
* 代际 = 每个 run 出生时抓一份号,写缓存/清位之前核对「我这一号还是不是当代」——不是当代的 run
|
|
49
|
+
* 只许**安静退场**,绝不许写、也绝不许清别人的位。号只增不减,`invalidate` 是唯一的推进者。
|
|
50
|
+
*/
|
|
51
|
+
const genByBase = new Map();
|
|
52
|
+
/** 该 base 的当代号(从没失效过 = 0)。 */
|
|
53
|
+
function capsGeneration(baseUrl) {
|
|
54
|
+
return genByBase.get(baseUrl) ?? 0;
|
|
55
|
+
}
|
|
22
56
|
/** 构造期 kick(async 幂等);probe = client.capabilities 薄闭包。 */
|
|
23
57
|
export function kickEngineCapsProbe(baseUrl, probe) {
|
|
24
58
|
if (!baseUrl || capsByBase.has(baseUrl) || inFlight.has(baseUrl))
|
|
25
59
|
return;
|
|
60
|
+
// 出生代际:落地时拿它与当代号核对(见 genByBase 头注)。
|
|
61
|
+
const gen = capsGeneration(baseUrl);
|
|
26
62
|
inFlight.add(baseUrl);
|
|
27
|
-
|
|
63
|
+
let wake = () => { };
|
|
64
|
+
const superseded = new Promise((resolve) => {
|
|
65
|
+
wake = () => resolve();
|
|
66
|
+
});
|
|
67
|
+
// settle 位用**手工兑现**的 promise,而不是 IIFE 的返回值 —— 这样它能在**调用方代码跑起来之前**
|
|
68
|
+
// 就落位。
|
|
69
|
+
// 🔴 顺序是判据的一部分(对抗复审第五轮 [medium] 采纳,2026-08-19):`probe` 是**调用方给的
|
|
70
|
+
// 闭包**,它同步段里完全可以回头调 `invalidateEngineCaps(baseUrl, 替代探测)`(两参原子形正是
|
|
71
|
+
// 为温切设计的,而温切逻辑就住在这种回调里)。旧写法先跑 IIFE(= 先跑调用方代码)、后
|
|
72
|
+
// `settleByBase.set` ⇒ 嵌套 kick 装好的**新代际**条目当场被外层这一行覆盖回旧条目,随后旧 run 的
|
|
73
|
+
// finally 又把它删掉 ⇒ `engineCapsSettled` 在替代探测仍在途时返回,能力读口报 false/undefined。
|
|
74
|
+
// 先落位再调用方代码 = 重入时外层没有任何机会回头覆盖别人。
|
|
75
|
+
let markSettled = () => { };
|
|
76
|
+
const settled = new Promise((resolve) => {
|
|
77
|
+
markSettled = () => resolve();
|
|
78
|
+
});
|
|
79
|
+
settleByBase.set(baseUrl, { gen, promise: settled, superseded, wake });
|
|
80
|
+
void (async () => {
|
|
28
81
|
try {
|
|
29
82
|
const caps = await probe();
|
|
83
|
+
// 🔴 被 invalidate 顶掉的旧 run 绝不写缓存 —— 它拿到的是**上一个引擎**的能力位。
|
|
84
|
+
if (gen !== capsGeneration(baseUrl))
|
|
85
|
+
return;
|
|
30
86
|
if (caps && typeof caps === 'object') {
|
|
31
87
|
capsByBase.set(baseUrl, caps);
|
|
32
88
|
}
|
|
@@ -35,11 +91,17 @@ export function kickEngineCapsProbe(baseUrl, probe) {
|
|
|
35
91
|
// 失败不缓存:下次 kick 再判
|
|
36
92
|
}
|
|
37
93
|
finally {
|
|
38
|
-
|
|
39
|
-
|
|
94
|
+
// 幂等闸位只由**当代** run 归还;旧 run 归还会把新 run 的在途位抹掉(重复探测)。
|
|
95
|
+
if (gen === capsGeneration(baseUrl)) {
|
|
96
|
+
inFlight.delete(baseUrl);
|
|
97
|
+
}
|
|
98
|
+
// settle 位按代际认领:表里那条不是我这一代的(= 已被新 run 顶掉)就别动它,
|
|
99
|
+
// 否则 engineCapsSettled 会对着仍在跑的新探测提前 resolve。
|
|
100
|
+
if (settleByBase.get(baseUrl)?.gen === gen)
|
|
101
|
+
settleByBase.delete(baseUrl);
|
|
102
|
+
markSettled();
|
|
40
103
|
}
|
|
41
104
|
})();
|
|
42
|
-
settleByBase.set(baseUrl, run);
|
|
43
105
|
}
|
|
44
106
|
/**
|
|
45
107
|
* 0.26.0(#225 件1 配套,消费请托自领):等**当前这一次**探测落地(成功或失败都算落地)。
|
|
@@ -47,11 +109,39 @@ export function kickEngineCapsProbe(baseUrl, probe) {
|
|
|
47
109
|
* 本函数只解决「kick 完只能盲猜轮询窗」的时序问题(慢响应被猜短的窗渲成「没有这条车道」)。
|
|
48
110
|
* 绝不 reject(探测失败=位维持未判,调用方按缺席降级)。
|
|
49
111
|
*/
|
|
50
|
-
export function engineCapsSettled(baseUrl) {
|
|
112
|
+
export async function engineCapsSettled(baseUrl) {
|
|
51
113
|
if (!baseUrl)
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
114
|
+
return;
|
|
115
|
+
// 🔴 **跨代际接力**(#307 S25 对抗复审第二轮 [medium] 采纳,2026-08-19)。此前本函数抓住**一条**
|
|
116
|
+
// promise 就不再回头看:调用方 W 抓的是旧代际探测 P0,随后 {@link invalidateEngineCaps} 推进代际
|
|
117
|
+
// 并起了 P1 —— P0 先落地就把 W 放走,而 P0 已被代际闸挡住不许写缓存 ⇒ W 当场读到空缓存,把
|
|
118
|
+
// **新引擎的能力位当成缺席**。这正是本批要消灭的那类假缺席,只是搬到了 settle 面上。
|
|
119
|
+
// 修 = 每等完一条就**重新求值**表里的当代条目([loop-termination-requires-reevaluation]):
|
|
120
|
+
// 表里换上了更新代际的那条就改等它。代际严格递增 ⇒ 循环必然终止;每轮都真 await 一条 promise ⇒
|
|
121
|
+
// 不是忙等。
|
|
122
|
+
//
|
|
123
|
+
// 🔴 **等的是「这条探测落地」或「代际变了」两者先到者**(第四轮 [high] 采纳)。只等 promise 是
|
|
124
|
+
// 不够的:引擎重启恰恰是最容易把旧探测**吊死**的时刻(旧进程没了,那个 fetch 可能永远不返回),
|
|
125
|
+
// 于是「P0 挂死 ⇒ 两参原子失效已经起了 P1 且 P1 已经把新能力位写进缓存 ⇒ 而 invalidate 之前取件的
|
|
126
|
+
// 等待者仍永久卡在 P0 上」——缓存里明明是对的,等待者却永远拿不到。代际信号把它叫醒去重新求值。
|
|
127
|
+
// ⚠️ 叫醒 **不是** 放行:醒来后照走本循环 —— 表里换上了新代际(两参原子形保证同拍就在)就接着等
|
|
128
|
+
// 新探测;没换(单参形、调用方这一刻没有替代探测)才返回,那时判据回到缓存位本身 = 未判 =
|
|
129
|
+
// 诚实缺席。所以「被叫醒」既不会漏掉新引擎的位,也不会硬等一个可能永远不来的 kick。
|
|
130
|
+
for (;;) {
|
|
131
|
+
const entry = settleByBase.get(baseUrl);
|
|
132
|
+
if (entry === undefined)
|
|
133
|
+
return;
|
|
134
|
+
const awaitedGen = entry.gen;
|
|
135
|
+
await Promise.race([
|
|
136
|
+
entry.promise.then(() => undefined, () => undefined),
|
|
137
|
+
entry.superseded,
|
|
138
|
+
]);
|
|
139
|
+
const next = settleByBase.get(baseUrl);
|
|
140
|
+
// 表里没有**更新代际**的一条 ⇒ 没得再等了(当代要么已落地、要么根本没起新探测:
|
|
141
|
+
// 那种情况下判据回到缓存位本身 = 未判 = 诚实缺席,由调用方按缺席降级)。
|
|
142
|
+
if (next === undefined || next.gen <= awaitedGen)
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
55
145
|
}
|
|
56
146
|
/** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。 */
|
|
57
147
|
export function engineCapTrue(baseUrl, key) {
|
|
@@ -73,9 +163,65 @@ export function engineCapString(baseUrl, key) {
|
|
|
73
163
|
const v = capsByBase.get(baseUrl)?.[key];
|
|
74
164
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
75
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* **生产失效口**(#307 双扫 S25,client-core 0.37.0):把该 base 的探测结果作废,
|
|
168
|
+
* 使**下一次** {@link kickEngineCapsProbe} 真正重探(而不是被幂等闸原样挡回)。
|
|
169
|
+
*
|
|
170
|
+
* 消费方 = **知道「这个 baseUrl 后面换了一个引擎进程」的那一层**,目前唯一一处是壳的引擎温切:
|
|
171
|
+
* cli `respawn` / `restartEngine` 成功后、重新构造 `createLiveConversationClient` **之前**调用。
|
|
172
|
+
* 库这一层看到的只有一个字符串 base,分辨不出对面是不是同一个进程,所以失效必须由上面显式下达 ——
|
|
173
|
+
* 见模块头注:靠 TTL 或「每次构造清」去猜,是把一个确定事实换成一个定时器。
|
|
174
|
+
*
|
|
175
|
+
* 语义与边界:
|
|
176
|
+
* · **推进代际**({@link genByBase}) + 清 `capsByBase`(已判结果) + 清 `inFlight`(在途去重位)。
|
|
177
|
+
* · **不 abort** 在途探测(没有可 abort 的把手,probe 是调用方给的薄闭包),改用代际让它
|
|
178
|
+
* **安静退场**:被顶掉的旧 run 落地后既不写缓存、也不归还任何位。所以本口在**探测在途时
|
|
179
|
+
* 调用是安全的** —— 旧引擎那次响应绝不会覆盖新引擎的能力位(2026-08-19 对抗复审 [high])。
|
|
180
|
+
* · `settleByBase` **不清**:{@link engineCapsSettled} 的语义是「等**当前这一次**探测落地」,
|
|
181
|
+
* 把在途 promise 抽走会让正在 await 的调用方立即拿到 resolve(假「已落地」)。旧 run 的
|
|
182
|
+
* settle 位在被新 run 顶掉后由代际认领保护,旧 run 的 `finally` 不会误删。
|
|
183
|
+
* · 空串 ⇒ no-op。**从没探过也从没失效过的 base ⇒ 真 no-op**(不留代际条目):那种 base 上
|
|
184
|
+
* 既无判定也无在途 run,没有任何陈旧写入可挡,留条目只会让任意串撑大表。
|
|
185
|
+
* · 绝不 throw(与本模块其余口同款:失效口在错误路径上响 = 把一个清理动作变成新的故障源)。
|
|
186
|
+
*
|
|
187
|
+
* 🔴 **推荐用两参形 `invalidateEngineCaps(baseUrl, probe)`**(对抗复审第三轮 [medium] 采纳,
|
|
188
|
+
* 2026-08-19)。单参形与「下一次 kick」之间有一个**真窗**:失效之后、新探测注册之前,如果旧代际
|
|
189
|
+
* 探测正好在这一拍落地,`settleByBase` 里已经没有更新代际的条目 ⇒ {@link engineCapsSettled} 把
|
|
190
|
+
* 等待者放走,而缓存刚被清空 ⇒ 等待者把**当代引擎的能力位读成缺席**。窗口只在调用方于失效与 kick
|
|
191
|
+
* 之间 `await` 了什么时才张开(同步块里 JS 单线程,旧探测的续体根本插不进来),但引擎温切本身就是
|
|
192
|
+
* 异步流程,所以它是可达的。
|
|
193
|
+
* 两参形把「推进代际」与「注册替代探测」放进**同一个同步块** ⇒ 窗口按构造不存在,等待者会被接力
|
|
194
|
+
* 到新探测上(见 {@link engineCapsSettled} 的跨代际接力)。壳的 respawn/restartEngine 应当用它。
|
|
195
|
+
* 单参形保留给「只想丢掉缓存、这一刻没有替代探测」的调用方 —— 那种情形下等待者读到**未判**
|
|
196
|
+
* 是诚实结局(判据永远是缓存位),不是缺陷:硬等一个可能永远不会来的 kick 才是。
|
|
197
|
+
*
|
|
198
|
+
* @param probe 可选的**替代探测**(与新引擎同一拍注册)。给了就等价于「失效 + 立刻 kick」,
|
|
199
|
+
* 且中间没有任何可插入点。
|
|
200
|
+
*/
|
|
201
|
+
export function invalidateEngineCaps(baseUrl, probe) {
|
|
202
|
+
if (!baseUrl)
|
|
203
|
+
return;
|
|
204
|
+
const hadState = capsByBase.has(baseUrl) ||
|
|
205
|
+
inFlight.has(baseUrl) ||
|
|
206
|
+
settleByBase.has(baseUrl) ||
|
|
207
|
+
genByBase.has(baseUrl);
|
|
208
|
+
if (hadState) {
|
|
209
|
+
genByBase.set(baseUrl, capsGeneration(baseUrl) + 1);
|
|
210
|
+
capsByBase.delete(baseUrl);
|
|
211
|
+
inFlight.delete(baseUrl);
|
|
212
|
+
// 代际变更信号:把等在旧代际上的调用方叫醒去重新求值(见 settleByBase 与 engineCapsSettled
|
|
213
|
+
// 头注)。叫醒 ≠ 放行;下面 kick 的注册是**同步**发生的,所以醒来的续体(微任务)必然已经能
|
|
214
|
+
// 看到替代探测,不会从空隙里溜走。
|
|
215
|
+
settleByBase.get(baseUrl)?.wake();
|
|
216
|
+
}
|
|
217
|
+
// 原子替代:同一同步块内注册新代际的探测(上面刚清了 caps/inFlight,kick 的幂等闸必放行)。
|
|
218
|
+
if (probe !== undefined)
|
|
219
|
+
kickEngineCapsProbe(baseUrl, probe);
|
|
220
|
+
}
|
|
76
221
|
/** 测试钩子。 */
|
|
77
222
|
export function __resetEngineCapsCacheForTests() {
|
|
78
223
|
capsByBase.clear();
|
|
79
224
|
inFlight.clear();
|
|
80
225
|
settleByBase.clear();
|
|
226
|
+
genByBase.clear();
|
|
81
227
|
}
|
|
@@ -187,7 +187,15 @@ export declare function deriveAgentLabel(agentType: string | undefined, agentNam
|
|
|
187
187
|
* 而"登记"本身要过门 —— 登记项必须是 core 认过的改名/墓碑词。
|
|
188
188
|
*/
|
|
189
189
|
export declare const CONTROL_TOOL_VERBS: ReadonlySet<string>;
|
|
190
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则按单行呈现载体规整后放行。
|
|
192
|
+
*
|
|
193
|
+
* #307:本函数是描述列的**主臂**(`projectDescription(...) || taskDescFromName(...)`),
|
|
194
|
+
* 修前它把 wire 原串原样返回 —— 描述列的规整只落在 fallback 臂的 `collapseLabel` 上,
|
|
195
|
+
* 主臂给值时那道规整根本不经过([assert-absence-check-fallback-layers] 同族形)。
|
|
196
|
+
* 🔴 压制判定仍按**原值** trim(判据集是 ASCII 动词名,规整不参与放行判定);规整只加在
|
|
197
|
+
* 返回的呈现串上。空/非空面不变:可见转义不产生空白,`|| fallback` 的真假面照旧。
|
|
198
|
+
*/
|
|
191
199
|
export declare function projectDescription(description: string | undefined): string;
|
|
192
200
|
/**
|
|
193
201
|
* `toolUses` —— 子代**累计**工具调用数。语义两条(接错会显示一个「看起来很合理」的错数字):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { collapseLabel, taskDescFromName } from '../fleetTaskDesc.js';
|
|
1
|
+
import { collapseLabel, escapeDisplayControlChars, taskDescFromName } from '../fleetTaskDesc.js';
|
|
2
2
|
import { rowIdTail } from '../workflow.js';
|
|
3
3
|
/**
|
|
4
4
|
* SDK `FleetTaskStatus` 的终态词汇 —— 非终态即在飞(未知未来值按在飞算,保守)。
|
|
@@ -121,10 +121,20 @@ export const CONTROL_TOOL_VERBS = new Set([
|
|
|
121
121
|
'ToolSearch',
|
|
122
122
|
'DeclareDone',
|
|
123
123
|
]);
|
|
124
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则按单行呈现载体规整后放行。
|
|
126
|
+
*
|
|
127
|
+
* #307:本函数是描述列的**主臂**(`projectDescription(...) || taskDescFromName(...)`),
|
|
128
|
+
* 修前它把 wire 原串原样返回 —— 描述列的规整只落在 fallback 臂的 `collapseLabel` 上,
|
|
129
|
+
* 主臂给值时那道规整根本不经过([assert-absence-check-fallback-layers] 同族形)。
|
|
130
|
+
* 🔴 压制判定仍按**原值** trim(判据集是 ASCII 动词名,规整不参与放行判定);规整只加在
|
|
131
|
+
* 返回的呈现串上。空/非空面不变:可见转义不产生空白,`|| fallback` 的真假面照旧。
|
|
132
|
+
*/
|
|
125
133
|
export function projectDescription(description) {
|
|
126
134
|
const d = description ?? '';
|
|
127
|
-
|
|
135
|
+
if (CONTROL_TOOL_VERBS.has(d.trim()))
|
|
136
|
+
return '';
|
|
137
|
+
return escapeDisplayControlChars(d);
|
|
128
138
|
}
|
|
129
139
|
// ── wire 值域过滤器(B6:类型面已到货,这里只剩「脏值不当真」)──────────────────────────────────
|
|
130
140
|
/**
|
package/dist/fleetTaskDesc.d.ts
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* src/sema/fleetTaskDesc.ts — footer fleet 行「label/描述列」纯投影助手(零依赖,从 fleetClient.ts 提出
|
|
6
6
|
* 供单测直取——fleetClient 拖 SDK/appState/notification 重图,无法轻量 bundle;行为与提出前逐字同源)。
|
|
7
7
|
*/
|
|
8
|
+
export declare function escapeDisplayControlChars(s: string): string;
|
|
8
9
|
/** collapse a wire label to a single trimmed line (drops embedded newlines/runs). 187 row labels are always
|
|
9
10
|
* one short line; the <FleetTree/> column (Math.min(28,…) + wrap="truncate") does the visible ellipsis, so we
|
|
10
|
-
* only normalize here — no double-ellipsis, faithful to the 187 render.
|
|
11
|
+
* only normalize here — no double-ellipsis, faithful to the 187 render.
|
|
12
|
+
* #307:折叠之后再过 {@link escapeDisplayControlChars} —— `\s+` 折叠管不到 C0 非空白段/C1(NEL)/
|
|
13
|
+
* 双向控制符/孤代理项,那四族原样透到终端就是「一行」这个结构账被绕过。空/非空判定不变
|
|
14
|
+
* (可见转义不产生空白,`|| fallback` 那些调用点的真假面照旧)。 */
|
|
11
15
|
export declare function collapseLabel(s: string): string;
|
|
12
16
|
/** CC row-body fallback(CC 真身锚 2026-07-20 人类剧本帧:`◯ general-purpose t1calc`——row body 从第一帧
|
|
13
17
|
* 起就是 Agent 工具的 description)。引擎子行帧不带 `description` 字段,任务描述只在 `name` 里,且有两形
|
package/dist/fleetTaskDesc.js
CHANGED
|
@@ -5,11 +5,48 @@
|
|
|
5
5
|
* src/sema/fleetTaskDesc.ts — footer fleet 行「label/描述列」纯投影助手(零依赖,从 fleetClient.ts 提出
|
|
6
6
|
* 供单测直取——fleetClient 拖 SDK/appState/notification 重图,无法轻量 bundle;行为与提出前逐字同源)。
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* 单行呈现载体的**危险字符可见化**(#307 呈现面规整族,2026-08-19)。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 本段注释与正则里的不可见/双向字符一律 `\uXXXX` 转义,绝不字面嵌入(字面形无法目测核实)。
|
|
12
|
+
*
|
|
13
|
+
* 为什么 `\s+` 折叠不够:JS 的 `\s` 只覆盖 `\t\n\r\f\v` + 空格 + `\u00a0` +
|
|
14
|
+
* `\u1680` + `\u2000-\u200a` + `\u2028\u2029` + `\u202f\u205f\u3000\ufeff`。
|
|
15
|
+
* **没被它覆盖、却会被终端执行**的还有四族:
|
|
16
|
+
* · C0 全段 + DEL(`\u0000-\u001f`、`\u007f`):裸 ESC/CSI 能改色、清行、移光标;
|
|
17
|
+
* · C1(`\u0080-\u009f`):NEL(`\u0085`)在终端上就是换行,却**不在** `\s` 里 ——
|
|
18
|
+
* 「fleet 行就一行」这个结构账被它一个字符绕过;
|
|
19
|
+
* · 双向控制符 + 行/段分隔符(`\u061c`、`\u200e\u200f`、`\u2028\u2029`、
|
|
20
|
+
* `\u202a-\u202e`、`\u2066-\u2069`):不是控制字符,却会被终端的双向重排/断行**执行**,
|
|
21
|
+
* 把 wire 给的行描述在屏上排成另一个样子;
|
|
22
|
+
* · 孤代理项:上游可能本来就送半只,或被列宽截断劈开。
|
|
23
|
+
*
|
|
24
|
+
* 形状选型=**可见转义**(`\uXXXX`)而非 `.` 占位:本函数的载体是 footer fleet 行的
|
|
25
|
+
* label/描述**单行列**,可见形既不引入伪行,也不让两个不同的 wire 值在屏上同形。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 射程只到呈现:不改「读不读得出」,也不参与任何放行判定。
|
|
28
|
+
* 🔴 顺序只在 collapseLabel 那侧有要求:`\s+` 折叠必须先跑(把 `\t\n\r` 等空白归一成
|
|
29
|
+
* 空格),反了会把换行渲成 `\u000A` 字面,「多行 name 塌一行」的既有行为就没了。
|
|
30
|
+
* 没经折叠的载体(fleetProjection.projectDescription)直接过本函数 —— `\t\n` 就渲成
|
|
31
|
+
* 可见转义,单行列里那才是对的(单行载体不许保 `\n`)。
|
|
32
|
+
*
|
|
33
|
+
* ⇄ 同族单源:壳仓 `src/sema/untrustedDisplayText.ts` 的 `cleanUntrustedScalar` 是同一判据的
|
|
34
|
+
* cli 侧实现(那份还带 `.` 占位与保排版两个变体),字符集与本函数**同集**。该单源日后上收到
|
|
35
|
+
* 本包时,本函数是它的座位;在那之前改一处必须改另一处(两侧各有逐码位同集断言看守)。
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
const DISPLAY_UNSAFE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
|
|
39
|
+
export function escapeDisplayControlChars(s) {
|
|
40
|
+
return s.replace(DISPLAY_UNSAFE, ch => `\\u${(ch.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, '0')}`);
|
|
41
|
+
}
|
|
8
42
|
/** collapse a wire label to a single trimmed line (drops embedded newlines/runs). 187 row labels are always
|
|
9
43
|
* one short line; the <FleetTree/> column (Math.min(28,…) + wrap="truncate") does the visible ellipsis, so we
|
|
10
|
-
* only normalize here — no double-ellipsis, faithful to the 187 render.
|
|
44
|
+
* only normalize here — no double-ellipsis, faithful to the 187 render.
|
|
45
|
+
* #307:折叠之后再过 {@link escapeDisplayControlChars} —— `\s+` 折叠管不到 C0 非空白段/C1(NEL)/
|
|
46
|
+
* 双向控制符/孤代理项,那四族原样透到终端就是「一行」这个结构账被绕过。空/非空判定不变
|
|
47
|
+
* (可见转义不产生空白,`|| fallback` 那些调用点的真假面照旧)。 */
|
|
11
48
|
export function collapseLabel(s) {
|
|
12
|
-
return s.replace(/\s+/g, ' ').trim();
|
|
49
|
+
return escapeDisplayControlChars(s.replace(/\s+/g, ' ').trim());
|
|
13
50
|
}
|
|
14
51
|
/** CC row-body fallback(CC 真身锚 2026-07-20 人类剧本帧:`◯ general-purpose t1calc`——row body 从第一帧
|
|
15
52
|
* 起就是 Agent 工具的 description)。引擎子行帧不带 `description` 字段,任务描述只在 `name` 里,且有两形
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
* {@link registerArmedGateFromQuestionId} 剥前缀取键(= callKey,与卡键同域)。
|
|
23
23
|
* · plan_review:`planReviewQuestionId(taskId)` 整串作键(保留前缀,与 taskId 直接作 ask 键
|
|
24
24
|
* 永不撞域);重开腿铸 `…#reopen-*`,归一化剥尾 —— 首呈与重开落同一键。
|
|
25
|
-
* 🔴 A-024.4(#250,#244 F1
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* 🔴 A-024.4(#250,#244 F1 起分代在包;2026-08-19 随 `ActiveRunPendingGate` 现值订正 ——
|
|
26
|
+
* 原句「wire 上没有 gate 实例位,[3664] pendingGate 只有 kind/decidePath」已过期):
|
|
27
|
+
* `ActiveRunPendingGate`(见 `adapter/runStream.ts`)今天已长到四位(kind/decidePath/
|
|
28
|
+
* governanceForced/**checkpointId**,#285 件2,0.36.0)—— `governanceForced` 是纯归因布尔,
|
|
29
|
+
* 不是身份;`checkpointId` 才是这材料上第一个够得上「实例位」气味的东西,但它自己的头注
|
|
30
|
+
* 明文划了界:**不是跨调用的稳定去重键**(同 session 可并存多条 pending checkpoint、server
|
|
31
|
+
* 选行是无序 `LIMIT 1`,同一情形连续两次 409 可能报出不同 id,也可能 id 不变而
|
|
32
|
+
* `activeTaskId` 已换),且缺席是「未诞生 / 校验未过 / 整只读取失败」三成因合流,读不出
|
|
33
|
+
* 「这不是同一道门」。⇒ 它不能安全替掉本节的按呈现代数分代 —— `pendingGateIsProvablyDifferent`
|
|
34
|
+
* 那种单向谓词(「id 变了 ⇒ 确实不同」)本节用不上:分代要答的是「这是不是同一张**卡**」的
|
|
35
|
+
* 双向问题,checkpointId 只背书单向的那半。plan 键因此仍按**呈现代数**分代(见
|
|
28
36
|
* {@link planReviewArmedKeyFor} / {@link notePlanReviewAnsweredFor}):0 代键与 canonical
|
|
29
37
|
* 字节同形,≥1 代接 `#g<N>` 尾;决断性作答推代(消费当代门),下一只门读回首见。
|
|
30
38
|
*
|
|
@@ -155,6 +155,26 @@ export declare class HitlSafetyError extends Error {
|
|
|
155
155
|
* 一边加一边不改另一边 = 新码在消费端被静默吞掉(比不加还坏:本地拦住了,判词却丢了)。 */
|
|
156
156
|
code: 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'empty_answer');
|
|
157
157
|
}
|
|
158
|
+
/** 测试钩:把超时类重试总窗调小(传 undefined 还原缺省)。 */
|
|
159
|
+
export declare function __setDecideTimeoutRetryBudgetForTests(ms?: number): void;
|
|
160
|
+
/**
|
|
161
|
+
* decide 出站在瞬断类失败上重试一次**仍未送达**(两发都没拿到引擎的语义答复)。
|
|
162
|
+
*
|
|
163
|
+
* 🔴 判别位契约:消费方(`toolApprovalWire` 的 catch 臂 / `parkResolver.surfaceGateAndDecide`)
|
|
164
|
+
* 据 `instanceof` 在 outcome 上 stamp `retryExhausted: true`,parkResolver 对该位走**重呈臂**
|
|
165
|
+
* (re-attach ⇒ durable 流重放 park ⇒ 同一张卡重新交给用户),不再合成 `hitl_unanswered` 把 turn
|
|
166
|
+
* 判死 —— run 仍 parked、pending 行仍可决,判死是三条出路里唯一不可逆的那条。
|
|
167
|
+
* 🔴 message 刻意避开 `isAlreadyResolvedGateReason` 的词表(no pending checkpoint / resolved /
|
|
168
|
+
* already / not found):被那把兜底尺误判成「已解决」会走成静默 reattach,判别位就白铸了 ——
|
|
169
|
+
* pure 门(hitl-gate-honesty F6-d)有常驻负控钉着这一条。
|
|
170
|
+
*/
|
|
171
|
+
export declare class DecideTransportRetryExhaustedError extends Error {
|
|
172
|
+
/** 实际发出的次数(网络断类 = 2:首发 + 单次重试;超时类 = 总窗内发出的全部)。 */
|
|
173
|
+
readonly attempts: number;
|
|
174
|
+
constructor(
|
|
175
|
+
/** 实际发出的次数(网络断类 = 2:首发 + 单次重试;超时类 = 总窗内发出的全部)。 */
|
|
176
|
+
attempts: number, lastFailureText: string);
|
|
177
|
+
}
|
|
158
178
|
/** The active gate the downstream stream most recently suspended on (null when running). */
|
|
159
179
|
export interface ActiveGate {
|
|
160
180
|
gate: CheckpointGate;
|