@sema-agent/client-core 0.36.0 → 0.38.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +2 -2
  3. package/dist/adapt/arms.js +43 -0
  4. package/dist/adapt.d.ts +1 -1
  5. package/dist/adapt.js +2 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +35 -2
  7. package/dist/adapter/activeRunSelfHeal.js +157 -4
  8. package/dist/adapter/downstream/eventToSdkMessage.js +99 -0
  9. package/dist/adapter/runStream.d.ts +14 -1
  10. package/dist/adapter/runStream.js +4 -0
  11. package/dist/engineCapsCache.d.ts +81 -3
  12. package/dist/engineCapsCache.js +183 -15
  13. package/dist/engineErrorCodes.d.ts +20 -0
  14. package/dist/engineErrorCodes.js +44 -0
  15. package/dist/fleet/fleetProjection.d.ts +43 -1
  16. package/dist/fleet/fleetProjection.js +53 -3
  17. package/dist/fleetTaskDesc.d.ts +5 -1
  18. package/dist/fleetTaskDesc.js +39 -2
  19. package/dist/hitl/armedGateRegistry.js +11 -3
  20. package/dist/hitl/hitlBridge.d.ts +20 -0
  21. package/dist/hitl/hitlBridge.js +215 -12
  22. package/dist/hitl/parkResolver.d.ts +1 -0
  23. package/dist/hitl/parkResolver.js +22 -2
  24. package/dist/hitl/toolApprovalWire.d.ts +41 -6
  25. package/dist/hitl/toolApprovalWire.js +40 -6
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +4 -0
  28. package/dist/model/modelSupplyRules.d.ts +102 -0
  29. package/dist/model/modelSupplyRules.js +149 -0
  30. package/dist/model/providerPresets.js +68 -12
  31. package/dist/request/taskRequest.js +11 -11
  32. package/dist/retryStatus.d.ts +38 -3
  33. package/dist/retryStatus.js +15 -5
  34. package/dist/seam.d.ts +48 -1
  35. package/dist/seam.js +7 -0
  36. package/dist/subagent/engineSubagentResume.d.ts +18 -0
  37. package/dist/subagent/engineSubagentResume.js +7 -0
  38. package/dist/toolResult.d.ts +8 -0
  39. package/dist/toolResult.js +15 -0
  40. package/docs/INTEGRATION-CLIENTS.md +69 -20
  41. package/package.json +4 -4
@@ -12,21 +12,77 @@
12
12
  * · kick 幂等(in-flight 去重),绝不 throw;
13
13
  * · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
14
14
  * · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
15
- * 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL——本缓存随
16
- * 每次 createLiveConversationClient 构造重 kick(引擎温切重启后新构造自然重探)。
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
- /** in-flight 探测的 settle 载体(finally 清;engineCapsSettled 消费)。 */
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
- const run = (async () => {
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
- inFlight.delete(baseUrl);
39
- settleByBase.delete(baseUrl);
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,17 +109,67 @@ 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 Promise.resolve();
53
- const p = settleByBase.get(baseUrl);
54
- return p ? p.then(() => undefined, () => undefined) : Promise.resolve();
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
- /** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。 */
57
- export function engineCapTrue(baseUrl, key) {
146
+ /**
147
+ * 同步读口(三态化):该 base 的 caps 里 key 的**可分辨**读数。语义逐条见 {@link EngineCapState}。
148
+ *
149
+ * `baseUrl` 缺席 ⇒ `unprobed`(没有 base 就没有任何一次探测,这不是「引擎说没有」)。
150
+ */
151
+ export function engineCapState(baseUrl, key) {
58
152
  if (!baseUrl)
59
- return false;
60
- return capsByBase.get(baseUrl)?.[key] === true;
153
+ return 'unprobed';
154
+ const caps = capsByBase.get(baseUrl);
155
+ if (caps === undefined)
156
+ return 'unprobed';
157
+ // 🔴 `in` 而不是 `!== undefined`:引擎显式发 `{"foo": undefined}` 在 JSON 上不可能,但本表也接
158
+ // 宿主注入的对象;`in` 问的是「键在不在」,正是 `absent` 这一档要答的问题。
159
+ if (!(key in caps))
160
+ return 'absent';
161
+ const v = caps[key];
162
+ if (v === true)
163
+ return 'true';
164
+ if (v === false)
165
+ return 'false';
166
+ return 'non_boolean';
167
+ }
168
+ /** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。
169
+ * 🔴 **语义一字不变**(#318 件③ 三态化后改为经 {@link engineCapState} 单源实现):放行面读的就是
170
+ * 这一口,四种「不知道」继续一律折成 `false` = fail-closed。要分辨成因请读 {@link engineCapState}。 */
171
+ export function engineCapTrue(baseUrl, key) {
172
+ return engineCapState(baseUrl, key) === 'true';
61
173
  }
62
174
  /**
63
175
  * 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
@@ -73,9 +185,65 @@ export function engineCapString(baseUrl, key) {
73
185
  const v = capsByBase.get(baseUrl)?.[key];
74
186
  return typeof v === 'string' && v.length > 0 ? v : undefined;
75
187
  }
188
+ /**
189
+ * **生产失效口**(#307 双扫 S25,client-core 0.37.0):把该 base 的探测结果作废,
190
+ * 使**下一次** {@link kickEngineCapsProbe} 真正重探(而不是被幂等闸原样挡回)。
191
+ *
192
+ * 消费方 = **知道「这个 baseUrl 后面换了一个引擎进程」的那一层**,目前唯一一处是壳的引擎温切:
193
+ * cli `respawn` / `restartEngine` 成功后、重新构造 `createLiveConversationClient` **之前**调用。
194
+ * 库这一层看到的只有一个字符串 base,分辨不出对面是不是同一个进程,所以失效必须由上面显式下达 ——
195
+ * 见模块头注:靠 TTL 或「每次构造清」去猜,是把一个确定事实换成一个定时器。
196
+ *
197
+ * 语义与边界:
198
+ * · **推进代际**({@link genByBase}) + 清 `capsByBase`(已判结果) + 清 `inFlight`(在途去重位)。
199
+ * · **不 abort** 在途探测(没有可 abort 的把手,probe 是调用方给的薄闭包),改用代际让它
200
+ * **安静退场**:被顶掉的旧 run 落地后既不写缓存、也不归还任何位。所以本口在**探测在途时
201
+ * 调用是安全的** —— 旧引擎那次响应绝不会覆盖新引擎的能力位(2026-08-19 对抗复审 [high])。
202
+ * · `settleByBase` **不清**:{@link engineCapsSettled} 的语义是「等**当前这一次**探测落地」,
203
+ * 把在途 promise 抽走会让正在 await 的调用方立即拿到 resolve(假「已落地」)。旧 run 的
204
+ * settle 位在被新 run 顶掉后由代际认领保护,旧 run 的 `finally` 不会误删。
205
+ * · 空串 ⇒ no-op。**从没探过也从没失效过的 base ⇒ 真 no-op**(不留代际条目):那种 base 上
206
+ * 既无判定也无在途 run,没有任何陈旧写入可挡,留条目只会让任意串撑大表。
207
+ * · 绝不 throw(与本模块其余口同款:失效口在错误路径上响 = 把一个清理动作变成新的故障源)。
208
+ *
209
+ * 🔴 **推荐用两参形 `invalidateEngineCaps(baseUrl, probe)`**(对抗复审第三轮 [medium] 采纳,
210
+ * 2026-08-19)。单参形与「下一次 kick」之间有一个**真窗**:失效之后、新探测注册之前,如果旧代际
211
+ * 探测正好在这一拍落地,`settleByBase` 里已经没有更新代际的条目 ⇒ {@link engineCapsSettled} 把
212
+ * 等待者放走,而缓存刚被清空 ⇒ 等待者把**当代引擎的能力位读成缺席**。窗口只在调用方于失效与 kick
213
+ * 之间 `await` 了什么时才张开(同步块里 JS 单线程,旧探测的续体根本插不进来),但引擎温切本身就是
214
+ * 异步流程,所以它是可达的。
215
+ * 两参形把「推进代际」与「注册替代探测」放进**同一个同步块** ⇒ 窗口按构造不存在,等待者会被接力
216
+ * 到新探测上(见 {@link engineCapsSettled} 的跨代际接力)。壳的 respawn/restartEngine 应当用它。
217
+ * 单参形保留给「只想丢掉缓存、这一刻没有替代探测」的调用方 —— 那种情形下等待者读到**未判**
218
+ * 是诚实结局(判据永远是缓存位),不是缺陷:硬等一个可能永远不会来的 kick 才是。
219
+ *
220
+ * @param probe 可选的**替代探测**(与新引擎同一拍注册)。给了就等价于「失效 + 立刻 kick」,
221
+ * 且中间没有任何可插入点。
222
+ */
223
+ export function invalidateEngineCaps(baseUrl, probe) {
224
+ if (!baseUrl)
225
+ return;
226
+ const hadState = capsByBase.has(baseUrl) ||
227
+ inFlight.has(baseUrl) ||
228
+ settleByBase.has(baseUrl) ||
229
+ genByBase.has(baseUrl);
230
+ if (hadState) {
231
+ genByBase.set(baseUrl, capsGeneration(baseUrl) + 1);
232
+ capsByBase.delete(baseUrl);
233
+ inFlight.delete(baseUrl);
234
+ // 代际变更信号:把等在旧代际上的调用方叫醒去重新求值(见 settleByBase 与 engineCapsSettled
235
+ // 头注)。叫醒 ≠ 放行;下面 kick 的注册是**同步**发生的,所以醒来的续体(微任务)必然已经能
236
+ // 看到替代探测,不会从空隙里溜走。
237
+ settleByBase.get(baseUrl)?.wake();
238
+ }
239
+ // 原子替代:同一同步块内注册新代际的探测(上面刚清了 caps/inFlight,kick 的幂等闸必放行)。
240
+ if (probe !== undefined)
241
+ kickEngineCapsProbe(baseUrl, probe);
242
+ }
76
243
  /** 测试钩子。 */
77
244
  export function __resetEngineCapsCacheForTests() {
78
245
  capsByBase.clear();
79
246
  inFlight.clear();
80
247
  settleByBase.clear();
248
+ genByBase.clear();
81
249
  }
@@ -59,11 +59,31 @@ export declare const CONFIG_STRATEGY_FIND_LIMIT_INVALID = "config.strategy_find_
59
59
  export declare const CONFIG_STRATEGY_MAX_SIZE_INVALID = "config.strategy_max_size_invalid";
60
60
  /** 未知的 limits 键(5.8.0 起:同时发新旧两代键会在这里当场失败,所以写面只发单一新形)。 */
61
61
  export declare const CONFIG_LIMIT_UNKNOWN_KEY = "config.limit_unknown_key";
62
+ /**
63
+ * `RunnerDeps.delegationEntryCaps`(core 5.48.0 / design/323,[4743] @cli 点名件)的坏值拒。
64
+ *
65
+ * 两种拒绝形:成员不是正整数(NaN / 0 / 负数 / 分数 / 非数),或**解出的一对**满足
66
+ * `maxConcurrent > maxCumulativePerSession`(一棵树不可能同时跑得比它这辈子能创建的还多 ——
67
+ * 这是矛盾不是偏好)。缺席成员取 CC 对齐缺省(20 / 200)。
68
+ * 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
69
+ */
70
+ export declare const CONFIG_DELEGATION_ENTRY_CAPS = "config.delegation_entry_caps";
62
71
  /**
63
72
  * 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
64
73
  * {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
65
74
  */
66
75
  export declare const CONFIG_REFUSAL_CODES: ReadonlySet<string>;
76
+ /** 并发帽:这棵树此刻活着的委派席位已达 `delegationEntryCaps.maxConcurrent`(CC 对齐缺省 20)。
77
+ * 🔴 处置 = **可等**(兄弟结束即有位),别渲成「配置要改」。 */
78
+ export declare const DELEGATION_CONCURRENCY_CAP = "delegation.concurrency_cap";
79
+ /** 会话累计帽:这棵树累计创建的委派席位已达 `maxCumulativePerSession`(CC 对齐缺省 200)。
80
+ * 🔴 处置 = **等也没用**(配额是这条会话这辈子的),别渲成「稍后重试」。 */
81
+ export declare const DELEGATION_SESSION_CAP = "delegation.session_cap";
82
+ /** 委派席位到限码识别表(**开集**:core 可能再加第三根轴)。两员处置不对称,消费点禁合并分支。 */
83
+ export declare const DELEGATION_CAP_CODES: ReadonlySet<string>;
84
+ /** `delegation.` 前缀谓词 —— **开集**判别(与 {@link isConfigRefusalCode} 同款)。缺席/空串 ⇒ false。
85
+ * 🔴 它只回答「这是不是一条委派席位拒绝」;**该等还是该换会话**必须按成员分,见两码各自的注释。 */
86
+ export declare function isDelegationCapCode(code: string | undefined): boolean;
67
87
  /** `config.` 前缀谓词 —— **开集**判别:5.10.0 之后每一波「不许静默折叠」都会往这一族加词,
68
88
  * 按前缀判的消费点不必跟车,按成员判的必须跟车。缺席/空串 ⇒ false。 */
69
89
  export declare function isConfigRefusalCode(code: string | undefined): boolean;
@@ -67,6 +67,15 @@ export const CONFIG_STRATEGY_FIND_LIMIT_INVALID = 'config.strategy_find_limit_in
67
67
  export const CONFIG_STRATEGY_MAX_SIZE_INVALID = 'config.strategy_max_size_invalid';
68
68
  /** 未知的 limits 键(5.8.0 起:同时发新旧两代键会在这里当场失败,所以写面只发单一新形)。 */
69
69
  export const CONFIG_LIMIT_UNKNOWN_KEY = 'config.limit_unknown_key';
70
+ /**
71
+ * `RunnerDeps.delegationEntryCaps`(core 5.48.0 / design/323,[4743] @cli 点名件)的坏值拒。
72
+ *
73
+ * 两种拒绝形:成员不是正整数(NaN / 0 / 负数 / 分数 / 非数),或**解出的一对**满足
74
+ * `maxConcurrent > maxCumulativePerSession`(一棵树不可能同时跑得比它这辈子能创建的还多 ——
75
+ * 这是矛盾不是偏好)。缺席成员取 CC 对齐缺省(20 / 200)。
76
+ * 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
77
+ */
78
+ export const CONFIG_DELEGATION_ENTRY_CAPS = 'config.delegation_entry_caps';
70
79
  /**
71
80
  * 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
72
81
  * {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
@@ -76,7 +85,42 @@ export const CONFIG_REFUSAL_CODES = new Set([
76
85
  CONFIG_STRATEGY_FIND_LIMIT_INVALID,
77
86
  CONFIG_STRATEGY_MAX_SIZE_INVALID,
78
87
  CONFIG_LIMIT_UNKNOWN_KEY,
88
+ CONFIG_DELEGATION_ENTRY_CAPS,
79
89
  ]);
90
+ // ── 委派席位到限族(core 5.48.0 design/323,[4743] @cli 点名的「两新 coded 拒绝」)──────────────
91
+ //
92
+ // 语义:一次 Task/SendMessage 委派因为**席位帽**被拒 —— 不是配置坏了(那是上面的
93
+ // `config.delegation_entry_caps`),也不是失败了,而是「现在不行」。两码**处置不同,禁合并**:
94
+ // · 并发帽 ⇒ **等**:活着的兄弟结束就有位,同一条命令过一会儿照样成;
95
+ // · 会话累计帽 ⇒ **等也没用**:这棵树这辈子的配额用完了,要么换会话要么抬 deps 旋钮。
96
+ // 把两者渲成同一句「委派失败」会让第一种情形的用户去改配置,第二种情形的用户去干等。
97
+ //
98
+ // 🔴 **载体如实登记(2026-08-21 亲验,别按 `tool_end.errorCode` 写消费码)**:core 把这两码铸进
99
+ // Task 工具结果体的 `details.error`(`subagent.ts` 的 `capRefusal`),而 core 的 `errorCode`
100
+ // 抬升腿(`runner/runtask.ts`)只读 `details.code` → `details.errorKind` **两个拼法**,
101
+ // `structuredFrom` 又要求 `details.type` 落在 `CC_DETAIL_TYPES` 里(这条 detail 连 `type` 都没有)
102
+ // ⇒ **今天这两码在 wire 上既不在 `tool_end.errorCode`、也不在 `structured`**,只剩模型面文案
103
+ // ("Sub-agent not started in background: …")。
104
+ // ⇒ 本词表**先立词、不落消费分支**:按文案反解正是本文件存在的理由要根除的东西
105
+ // ([cross-repo-fix-at-source-constitution]:载体缺口在 core,下游不许侧路补救)。
106
+ // 上游诉求已登记(见 CHANGELOG 0.38.0 段):core 补 `details.code` 孪生拼法(或抬升腿兼读
107
+ // `details.error`)之后,消费分支在本包同批接上,**判定归本包、文案归端**。
108
+ /** 并发帽:这棵树此刻活着的委派席位已达 `delegationEntryCaps.maxConcurrent`(CC 对齐缺省 20)。
109
+ * 🔴 处置 = **可等**(兄弟结束即有位),别渲成「配置要改」。 */
110
+ export const DELEGATION_CONCURRENCY_CAP = 'delegation.concurrency_cap';
111
+ /** 会话累计帽:这棵树累计创建的委派席位已达 `maxCumulativePerSession`(CC 对齐缺省 200)。
112
+ * 🔴 处置 = **等也没用**(配额是这条会话这辈子的),别渲成「稍后重试」。 */
113
+ export const DELEGATION_SESSION_CAP = 'delegation.session_cap';
114
+ /** 委派席位到限码识别表(**开集**:core 可能再加第三根轴)。两员处置不对称,消费点禁合并分支。 */
115
+ export const DELEGATION_CAP_CODES = new Set([
116
+ DELEGATION_CONCURRENCY_CAP,
117
+ DELEGATION_SESSION_CAP,
118
+ ]);
119
+ /** `delegation.` 前缀谓词 —— **开集**判别(与 {@link isConfigRefusalCode} 同款)。缺席/空串 ⇒ false。
120
+ * 🔴 它只回答「这是不是一条委派席位拒绝」;**该等还是该换会话**必须按成员分,见两码各自的注释。 */
121
+ export function isDelegationCapCode(code) {
122
+ return typeof code === 'string' && code.startsWith('delegation.');
123
+ }
80
124
  /** `config.` 前缀谓词 —— **开集**判别:5.10.0 之后每一波「不许静默折叠」都会往这一族加词,
81
125
  * 按前缀判的消费点不必跟车,按成员判的必须跟车。缺席/空串 ⇒ false。 */
82
126
  export function isConfigRefusalCode(code) {
@@ -93,6 +93,18 @@ export interface FleetTaskView {
93
93
  path: string;
94
94
  edits: number;
95
95
  }>;
96
+ /**
97
+ * #261 §2① 代际号(0.38.0 提货补投;取值规则见 {@link wireCycleSeq})。
98
+ * 同 id 帧 `cycleSeq` 更大 ⇒ **复活**(新代际,行内累计量重置);更小 ⇒ 前代迟到帧,忽略;
99
+ * **缺席 ⇒ 这条行没有代际概念**(不是第一代)。
100
+ */
101
+ cycleSeq?: number;
102
+ /**
103
+ * #261 §2② 非亲报终态的投影者(0.38.0 提货补投;取值规则见 {@link wireRetiredBy})。
104
+ * **在场 = 这条终态是对账腿从 durable run 行读出来的**(发布方死了),缺席 = 发布方亲报。
105
+ * 读侧开集,未知词通渲。
106
+ */
107
+ retiredBy?: string;
96
108
  }
97
109
  /** 终态行帧 / bg 通知帧共用的 usage 形(SDK `FleetTaskRow.usage` ≡ `FleetBgNotification.usage`)。 */
98
110
  export interface FleetRowUsage {
@@ -187,7 +199,15 @@ export declare function deriveAgentLabel(agentType: string | undefined, agentNam
187
199
  * 而"登记"本身要过门 —— 登记项必须是 core 认过的改名/墓碑词。
188
200
  */
189
201
  export declare const CONTROL_TOOL_VERBS: ReadonlySet<string>;
190
- /** 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则原样。 */
202
+ /**
203
+ * 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则按单行呈现载体规整后放行。
204
+ *
205
+ * #307:本函数是描述列的**主臂**(`projectDescription(...) || taskDescFromName(...)`),
206
+ * 修前它把 wire 原串原样返回 —— 描述列的规整只落在 fallback 臂的 `collapseLabel` 上,
207
+ * 主臂给值时那道规整根本不经过([assert-absence-check-fallback-layers] 同族形)。
208
+ * 🔴 压制判定仍按**原值** trim(判据集是 ASCII 动词名,规整不参与放行判定);规整只加在
209
+ * 返回的呈现串上。空/非空面不变:可见转义不产生空白,`|| fallback` 的真假面照旧。
210
+ */
191
211
  export declare function projectDescription(description: string | undefined): string;
192
212
  /**
193
213
  * `toolUses` —— 子代**累计**工具调用数。语义两条(接错会显示一个「看起来很合理」的错数字):
@@ -222,6 +242,28 @@ export declare function wireEditedFiles(r: Pick<FleetTaskRow, 'editedFiles'>): A
222
242
  }> | undefined;
223
243
  /** 终态四键之 `stoppedBy`(开放枚举 verbatim:"user"/"parent"/"system"/…)。 */
224
244
  export declare function wireStoppedBy(r: Pick<FleetTaskRow, 'stoppedBy'>): string | undefined;
245
+ /**
246
+ * #261 §2① `cycleSeq`(server ≥7.25.0 / core 5.36.0 #258;SDK **7.2.0** 才把它声明进
247
+ * `FleetTaskRow` ⇒ 本包 0.38.0 提货补投)—— 这一行的**代际号**,与 `FleetBgNotification.seq` /
248
+ * `task_progress.seq` **同域同轴**(fresh spawn 就是 cycle 1,每次 SendMessage 复活翻 +1)。
249
+ *
250
+ * 🔴 **缺席 = 「无此概念」,不是「第一代」**(SDK 头注逐字):没有 `a*` registry 行的 run ——
251
+ * 同步委派子代 / workflow `wa*` agent / **顶层 run 行** —— 根本没有代际。把缺席读成 1 的消费端
252
+ * 会把「首帧迟到」误判成「复活」。所以这里对**非正整数**一律整键缺席(0 / 负数 / 非有限数 /
253
+ * 非整数都不是合法代际号),绝不 `?? 0`、绝不 `?? 1`。
254
+ */
255
+ export declare function wireCycleSeq(r: Pick<FleetTaskRow, 'cycleSeq'>): number | undefined;
256
+ /**
257
+ * #261 §2② `retiredBy`(server ≥7.25.0;SDK 7.2.0 声明)—— 这一帧的终态**不是发布方亲报**,
258
+ * 而是对账腿从 durable run 行**投影**出来的(发布方死了,行本会永久僵在活跃集里当幽灵)。
259
+ *
260
+ * 🔴 **发布方亲报的终态帧恒不带此键** ⇒ 两种终态在 wire 上可判:要区分「引擎说它完了」与
261
+ * 「我们从库里读出来它完了」时,这是**唯一**的判据([ghost-rows-need-upstream-liveness] 的
262
+ * wire 侧对位物 —— 此前本层把这一位整个丢掉,两种终态在视图上同形)。
263
+ * service 侧今天是单词闭集(`"reconcile"`),**读侧开集**:未知词原样透传(将来的第二个投影者
264
+ * 会是一个新词而不是改义),消费端 branch 已知值 + 通渲兜底,**绝不**按成员判死。
265
+ */
266
+ export declare function wireRetiredBy(r: Pick<FleetTaskRow, 'retiredBy'>): string | undefined;
225
267
  /** 终态四键之 `resumable`(仅 bg 子代有源;非 boolean ⇒ 缺席,**不当 false**)。 */
226
268
  export declare function wireResumable(r: Pick<FleetTaskRow, 'resumable'>): boolean | undefined;
227
269
  export interface ProjectTasksOptions {
@@ -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
- /** 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则原样。 */
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
- return CONTROL_TOOL_VERBS.has(d.trim()) ? '' : d;
135
+ if (CONTROL_TOOL_VERBS.has(d.trim()))
136
+ return '';
137
+ return escapeDisplayControlChars(d);
128
138
  }
129
139
  // ── wire 值域过滤器(B6:类型面已到货,这里只剩「脏值不当真」)──────────────────────────────────
130
140
  /**
@@ -208,6 +218,34 @@ export function wireStoppedBy(r) {
208
218
  const v = r.stoppedBy;
209
219
  return typeof v === 'string' && v.length > 0 ? v : undefined;
210
220
  }
221
+ /**
222
+ * #261 §2① `cycleSeq`(server ≥7.25.0 / core 5.36.0 #258;SDK **7.2.0** 才把它声明进
223
+ * `FleetTaskRow` ⇒ 本包 0.38.0 提货补投)—— 这一行的**代际号**,与 `FleetBgNotification.seq` /
224
+ * `task_progress.seq` **同域同轴**(fresh spawn 就是 cycle 1,每次 SendMessage 复活翻 +1)。
225
+ *
226
+ * 🔴 **缺席 = 「无此概念」,不是「第一代」**(SDK 头注逐字):没有 `a*` registry 行的 run ——
227
+ * 同步委派子代 / workflow `wa*` agent / **顶层 run 行** —— 根本没有代际。把缺席读成 1 的消费端
228
+ * 会把「首帧迟到」误判成「复活」。所以这里对**非正整数**一律整键缺席(0 / 负数 / 非有限数 /
229
+ * 非整数都不是合法代际号),绝不 `?? 0`、绝不 `?? 1`。
230
+ */
231
+ export function wireCycleSeq(r) {
232
+ const v = r.cycleSeq;
233
+ return typeof v === 'number' && Number.isInteger(v) && v > 0 ? v : undefined;
234
+ }
235
+ /**
236
+ * #261 §2② `retiredBy`(server ≥7.25.0;SDK 7.2.0 声明)—— 这一帧的终态**不是发布方亲报**,
237
+ * 而是对账腿从 durable run 行**投影**出来的(发布方死了,行本会永久僵在活跃集里当幽灵)。
238
+ *
239
+ * 🔴 **发布方亲报的终态帧恒不带此键** ⇒ 两种终态在 wire 上可判:要区分「引擎说它完了」与
240
+ * 「我们从库里读出来它完了」时,这是**唯一**的判据([ghost-rows-need-upstream-liveness] 的
241
+ * wire 侧对位物 —— 此前本层把这一位整个丢掉,两种终态在视图上同形)。
242
+ * service 侧今天是单词闭集(`"reconcile"`),**读侧开集**:未知词原样透传(将来的第二个投影者
243
+ * 会是一个新词而不是改义),消费端 branch 已知值 + 通渲兜底,**绝不**按成员判死。
244
+ */
245
+ export function wireRetiredBy(r) {
246
+ const v = r.retiredBy;
247
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
248
+ }
211
249
  /** 终态四键之 `resumable`(仅 bg 子代有源;非 boolean ⇒ 缺席,**不当 false**)。 */
212
250
  export function wireResumable(r) {
213
251
  return typeof r.resumable === 'boolean' ? r.resumable : undefined;
@@ -247,6 +285,8 @@ export function projectTasks(allRows, opts) {
247
285
  const usage = wireRowUsage(r);
248
286
  const resumable = wireResumable(r);
249
287
  const editedFiles = wireEditedFiles(r);
288
+ const cycleSeq = wireCycleSeq(r);
289
+ const retiredBy = wireRetiredBy(r);
250
290
  const task = {
251
291
  id: r.id,
252
292
  name: deriveAgentLabel(r.agentType, r.agentName, r.name, r.id),
@@ -276,6 +316,11 @@ export function projectTasks(allRows, opts) {
276
316
  ...(usage !== undefined ? { usage } : {}),
277
317
  ...(resumable !== undefined ? { resumable } : {}),
278
318
  ...(editedFiles !== undefined ? { editedFiles } : {}),
319
+ // #261 §2 两位:代际号与「非亲报终态」判别位。两者都是**在场才落键** —— cycleSeq 缺席是
320
+ // 「这条行没有代际概念」、retiredBy 缺席是「发布方亲报」,任何一个补默认值都会把一个诚实
321
+ // 缺席翻译成一句假话。
322
+ ...(cycleSeq !== undefined ? { cycleSeq } : {}),
323
+ ...(retiredBy !== undefined ? { retiredBy } : {}),
279
324
  };
280
325
  return task;
281
326
  });
@@ -331,6 +376,8 @@ const FLEET_TASK_VIEW_KEY_TUPLE = [
331
376
  'usage',
332
377
  'resumable',
333
378
  'editedFiles',
379
+ 'cycleSeq',
380
+ 'retiredBy',
334
381
  ];
335
382
  /** `FleetTaskView` 的键名清单(运行期物;编译期与 `keyof FleetTaskView` 双向等值)。 */
336
383
  export const FLEET_TASK_VIEW_KEYS = FLEET_TASK_VIEW_KEY_TUPLE;
@@ -372,6 +419,9 @@ const FLEET_TASK_ROW_WIRE_KEY_TUPLE = [
372
419
  'usage',
373
420
  'resumable',
374
421
  'editedFiles',
422
+ // SDK 7.2.0 新声明的两位(#261 §2,server ≥7.25.0 早已在 wire 上 —— 这是**类型跟车**不是新能力)。
423
+ 'cycleSeq',
424
+ 'retiredBy',
375
425
  ];
376
426
  /** SDK `FleetTaskRow` 的**入口**键名清单(编译期与 `keyof FleetTaskRow` 双向等值 ⇒ SDK 加一个
377
427
  * wire 键,本元组不跟就编译红;跟了之后覆盖账那条腿再逼你表态「投不投」)。 */
@@ -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` 里,且有两形
@@ -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 起分代在包):wire 上没有 gate 实例位([3664] pendingGate 只有
26
- * kind/decidePath),同 run 推进到第二只 plan 门时,恒定键会让 firstSight 恒 false ⇒ 对一张
27
- * 从未呈现过的新卡编造「被关过」历史。plan 键因此按**呈现代数**分代(
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;