@sema-agent/client-core 0.50.0 → 0.52.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.
@@ -378,7 +378,16 @@ const TOOL_END_ARMS = [
378
378
  return undefined;
379
379
  led.markEnded(callId);
380
380
  led.dropHeld(callId);
381
- surfaceForCurrentSession()?.surfaceClassifierDeny(typeof ev.toolName === 'string' ? ev.toolName : 'tool', verdict);
381
+ // L-69⑨(0.52.0):把**这一只 call 的现场**一并交给宿主 —— 修前只给 `toolName`,于是端的
382
+ // Recent Denials 三行同名(三条被拒的 Bash 一个样)、retry 粒度只能落到「整个工具」。
383
+ // 🔴 args 是 **UNTRUSTED** wire 值:本层原样搬运(不渲不截不消毒),展示消毒归端。
384
+ // 🔴 args 取自**在飞表**(`noteStart`,每张 tool_start 都记、有界、收口即删)——`gatedStartArgs`
385
+ // 在这里读不到:分类器 deny 的工具通常**没被 gate**(引擎侧分类器直接 block,一张
386
+ // tool_approval 帧都不出),那张表里根本没有它。
387
+ // 🔴 缺席不铸键(`exactOptionalPropertyTypes`):台账里没有 ⇒ `ctx` 只带 `toolCallId`,
388
+ // 端读到的是诚实缺席,不是「args 是 undefined」。
389
+ const startArgs = led.startArgs().get(callId);
390
+ surfaceForCurrentSession()?.surfaceClassifierDeny(typeof ev.toolName === 'string' ? ev.toolName : 'tool', verdict, { toolCallId: callId, ...(startArgs !== undefined ? { args: startArgs } : {}) });
382
391
  hostLog('debug', `liveHitlAskWire: classifier deny verdict on ${callId} (${String(ev.toolName)}) — labeled render + notice`);
383
392
  return { kind: 'yield', events: [{ ...ev, output: verdict.message, structured: undefined }] };
384
393
  },
@@ -528,6 +537,15 @@ async function routeToolApprovalFrame(ev, ctx) {
528
537
  return { kind: 'skip' };
529
538
  }
530
539
  function routeToolStart(ev, callId, led) {
540
+ // L-69⑨(0.52.0):**所有** tool_start 都留一份 args 快照(有界在飞表,收口即删)。
541
+ // 🔴 与下面那张 gated 表刻意**不合表**:「表里有」绝不等于「被 gate 过」——两张表的成员关系、
542
+ // 生命周期、容量纪律都不同(逐条理由见 `gateLedger.ts` 的 `noteStart` 头注)。
543
+ // 这一份服务的是分类器 deny 这类**没走 gate**的收口现场(gated 表里根本没有它们)。
544
+ // 🔴 **必须排在 `markStarted` 闸之前**(异源对抗复审 R1 finding② 采纳):那道闸是**渲染**去重
545
+ // (durable 重放的已渲 call 不再上屏),而记账不是渲染。排在闸之后 ⇒ 重放帧一律早退 ⇒
546
+ // `noteStart` 的「先删再写」刷新在**生产路径上永远走不到**,只有直调台账的测试碰得着;
547
+ // 而重放恰恰是被有界表淘汰过的那些 call 唯一一次把快照拿回来的机会。
548
+ led.noteStart(callId, ev);
531
549
  if (!led.markStarted(callId))
532
550
  return { kind: 'skip' }; // durable 重放的已渲 call
533
551
  if (isGatedToolName(ev.toolName)) {
@@ -545,13 +563,24 @@ function routeToolStart(ev, callId, led) {
545
563
  return { kind: 'yield', events: [ev] };
546
564
  }
547
565
  function routeToolEnd(ev, callId, led) {
548
- if (led.isEnded(callId))
566
+ if (led.isEnded(callId)) {
567
+ // L-69⑨(0.52.0):已收口的 call **无论走哪条路**都不该留在在飞表里。这一行是上面
568
+ // 「noteStart 排在渲染去重闸之前」的对偶:durable 重放会把 start 再送一遍(于是快照被重新
569
+ // 加回来),而重放的 end 在这里早退 ⇒ 少了这一行,那一条就要一直挂到被容量淘汰为止。
570
+ led.dropStartArgs(callId);
549
571
  return { kind: 'skip' }; // durable 重放/park 复写的已收口 call
572
+ }
550
573
  led.dropFsCall(callId);
551
574
  for (const arm of TOOL_END_ARMS) {
552
575
  const action = arm.run(ev, callId, led);
553
- if (action)
576
+ if (action) {
577
+ // L-69⑨(0.52.0):在飞窗到此为止 —— 收口帧已经判完(臂**在这一行之前**跑,分类器 deny 臂
578
+ // 因此还读得到 args),这一条快照从此没有消费者。
579
+ // 🔴 释放点必须在**臂之后**:挪到循环之前 ⇒ deny 臂拿到的恒是缺席(修前形);挪到 `return`
580
+ // 的调用方 ⇒ 五条臂各自 return,释放就要抄五遍(而漏抄的那条就是泄漏点)。
581
+ led.dropStartArgs(callId);
554
582
  return action;
583
+ }
555
584
  }
556
585
  // 不可达:兜底臂 generic-close 无条件命中。真走到这里 = 有人给它加了早退条件,
557
586
  // 那样 tool_end 会被整帧静默丢弃(用户看不到任何收口),必须当场喊出来而不是 `skip`。
@@ -61,6 +61,29 @@ export interface GateLedger {
61
61
  * 审批 payload,且台账持帧延长了整帧生命周期。载荷仍是 UNTRUSTED unknown,台账只搬运不解释。 */
62
62
  noteGatedStart(callId: string, ev: AgentEvent): void;
63
63
  gatedStartArgs(): ReadonlyMap<string, unknown>;
64
+ /**
65
+ * 每一张 `tool_start` 的 `args` 快照,**不论这只工具有没有被 gate**。
66
+ *
67
+ * 🔴 为什么必须与 {@link gatedStartArgs} **分成两张表**(而不是「合表 + 用有没有 args 判 gated」):
68
+ * 「被 gate 过」是**另一个集合**,两者的成员关系没有任何蕴含 —— 分类器 deny 的工具通常
69
+ * **没有**被 gate(引擎侧分类器直接 block,一张 `tool_approval` 帧都不出),而 gate 过的工具
70
+ * 也可能整场没有 args。把「表里有这一条」读成「它被 gate 过」,等于拿一个搬运位当身份位
71
+ * ([provenance-needs-wire-fact-not-inference] 同族);合表之后总有人会这么读,所以不合。
72
+ * 两表的**生命周期**也刻意不同,见下:
73
+ * · {@link gatedStartArgs}:审批 payload 的一手源,**整个 turn 不删**(park / durable 重放
74
+ * 之后卡面还要读它),条目数被「真被 gate 的工具数」天然约束;
75
+ * · 本表:每一张 tool_start 都进,所以**必须有界**(见 {@link START_ARGS_MAX_ENTRIES}),
76
+ * 且 `tool_end` 收口当拍就删(见 {@link dropStartArgs})—— 它只服务「这只 call 还没收口」
77
+ * 那段窗口内的消费者(今天唯一一个:分类器 deny 臂给宿主的 `ctx.args`)。
78
+ *
79
+ * 🔴 存的是 **tool_start 当拍的 `args` 快照**而非帧引用,理由与 {@link noteGatedStart} 逐字同款。
80
+ * 载荷是 **UNTRUSTED** wire 值:台账只搬运,不解释、不渲染、不截断。
81
+ */
82
+ noteStart(callId: string, ev: AgentEvent): void;
83
+ /** 本 call 的 tool_start args 快照;没记过 / 已收口 / 已被有界淘汰 ⇒ 表里读不到。 */
84
+ startArgs(): ReadonlyMap<string, unknown>;
85
+ /** `tool_end` 收口当拍释放这一条(在飞窗结束)。不在表里 ⇒ no-op。 */
86
+ dropStartArgs(callId: string): void;
64
87
  isEnded(callId: string): boolean;
65
88
  markEnded(callId: string): void;
66
89
  /**
@@ -237,5 +260,36 @@ export interface GateLedger {
237
260
  /** 判据链上出现了真进展(park 真被决断 / 重探把新坐标的卡呈出去了)⇒ 连续计数归零。 */
238
261
  resetAlreadyResolvedGate(): void;
239
262
  }
263
+ /**
264
+ * {@link GateLedger.noteStart} 那张在飞表的**容量上限**(L-69⑨,0.52.0)。
265
+ *
266
+ * 🔴 为什么必须有界:本表的写口是**每一张** `tool_start`,而它的删口(`tool_end` 收口)在两种真实
267
+ * 形态下不会来 —— ①一个 turn 里 `tool_end` 先于本连接开始(durable re-attach 只消费 `runs.events`
268
+ * 的一段);②run 被中止 / 连接断掉,在飞 call 的收口帧永远不到。无界表在长流多工具的 turn 上
269
+ * 条目数会一直涨。
270
+ *
271
+ * 🔴 **上限量的是条目数(= 保留的引用数),不是字节数**(异源对抗复审 R1 finding① 采纳的措辞订正)。
272
+ * 本表存的是 `ev.args` 的**引用**,不是拷贝 —— 而同一个 `ev` 在这一拍**照常 yield 下去**
273
+ * (`frameRouter.routeToolStart` 返回 `{kind:'yield', events:[ev]}`),投影成 `SDKMessage` 之后
274
+ * 那份入参在端的转录面上本来就活着。所以本表对**驻留字节**的增量是「一个指针 × 条目数」,
275
+ * 它不复制载荷、也不是这条链上第一个持有它的容器:同文件的 {@link GateLedger.noteGatedStart}
276
+ * 从本批之前就持着同一形的引用,而且**整个 turn 不删、连条目数都不设上限**。
277
+ * ⚠️ 仍然如实说清**没有**守住的那一格:单条载荷本身**没有**字节预算(一次 `Write` 的
278
+ * `content` 有多大,这里就跟着多引用多大一份多久)。真要按字节记账得先定义「多大算大」并同批给出
279
+ * 超限时的降级形(截断 = 交出一份假入参,不许;整条不留 = 那只 call 的 deny 现场退回诚实缺席),
280
+ * 属独立一件;本批**不做单边近似**,如实登记在此。
281
+ * 🔴 为什么是 **256** 而不是更小:上限守的是「条目数无界增长」,而它必须**远大于**同一时刻真正在飞的
282
+ * call 数,否则淘汰会打到还没收口的 call 身上(那正是本表要服务的对象)。引擎侧同一 turn 的并发
283
+ * 工具批今天是个位数量级,单批 `tool_start` 连发也在两位数以内;256 给出的是两个数量级的余量。
284
+ * 🔴 淘汰**按 FIFO 丢最早**(`Map` 的插入序):最早入表的那一条是**最久没有收口**的,它要么已经因为
285
+ * 上面两种形态永远不会收口,要么它的消费窗(收口当拍)早就过去了 —— 丢它的代价最小。绝不按
286
+ * 「随便丢一个」或整表清空(整表清空会把同批在飞的兄弟一起打掉)。
287
+ * 🔴 **淘汰打到仍在飞的 call 时会怎样(成文的取舍,不是漏)**:那只 call 后来被分类器拒绝的话,
288
+ * 宿主拿到的 `ctx` 只有 `toolCallId`、没有 `args` —— 即**诚实缺席**,端按可选处理照常降级渲
289
+ * (契约见 `docs/INTEGRATION-CLIENTS.md` §15c 第 3 条)。这条路上唯一的替代品是「不淘汰」,
290
+ * 而那就是无界表。durable 重放会把 `tool_start` 再送一遍,那是被淘汰项**拿回快照**的机会 ——
291
+ * 所以 `frameRouter.routeToolStart` 把本动词排在渲染去重闸**之前**(见该站点注)。
292
+ */
293
+ export declare const START_ARGS_MAX_ENTRIES = 256;
240
294
  /** 造一份 turn 级 gate 台账(**不是单例**,见文件头注)。 */
241
295
  export declare function createGateLedger(): GateLedger;
@@ -9,6 +9,37 @@
9
9
  * additive 键保留兼容(裁定原文)。
10
10
  */
11
11
  export const SEMA_COLLATERAL_ABORT_KEY = '_sema_collateral_abort';
12
+ /**
13
+ * {@link GateLedger.noteStart} 那张在飞表的**容量上限**(L-69⑨,0.52.0)。
14
+ *
15
+ * 🔴 为什么必须有界:本表的写口是**每一张** `tool_start`,而它的删口(`tool_end` 收口)在两种真实
16
+ * 形态下不会来 —— ①一个 turn 里 `tool_end` 先于本连接开始(durable re-attach 只消费 `runs.events`
17
+ * 的一段);②run 被中止 / 连接断掉,在飞 call 的收口帧永远不到。无界表在长流多工具的 turn 上
18
+ * 条目数会一直涨。
19
+ *
20
+ * 🔴 **上限量的是条目数(= 保留的引用数),不是字节数**(异源对抗复审 R1 finding① 采纳的措辞订正)。
21
+ * 本表存的是 `ev.args` 的**引用**,不是拷贝 —— 而同一个 `ev` 在这一拍**照常 yield 下去**
22
+ * (`frameRouter.routeToolStart` 返回 `{kind:'yield', events:[ev]}`),投影成 `SDKMessage` 之后
23
+ * 那份入参在端的转录面上本来就活着。所以本表对**驻留字节**的增量是「一个指针 × 条目数」,
24
+ * 它不复制载荷、也不是这条链上第一个持有它的容器:同文件的 {@link GateLedger.noteGatedStart}
25
+ * 从本批之前就持着同一形的引用,而且**整个 turn 不删、连条目数都不设上限**。
26
+ * ⚠️ 仍然如实说清**没有**守住的那一格:单条载荷本身**没有**字节预算(一次 `Write` 的
27
+ * `content` 有多大,这里就跟着多引用多大一份多久)。真要按字节记账得先定义「多大算大」并同批给出
28
+ * 超限时的降级形(截断 = 交出一份假入参,不许;整条不留 = 那只 call 的 deny 现场退回诚实缺席),
29
+ * 属独立一件;本批**不做单边近似**,如实登记在此。
30
+ * 🔴 为什么是 **256** 而不是更小:上限守的是「条目数无界增长」,而它必须**远大于**同一时刻真正在飞的
31
+ * call 数,否则淘汰会打到还没收口的 call 身上(那正是本表要服务的对象)。引擎侧同一 turn 的并发
32
+ * 工具批今天是个位数量级,单批 `tool_start` 连发也在两位数以内;256 给出的是两个数量级的余量。
33
+ * 🔴 淘汰**按 FIFO 丢最早**(`Map` 的插入序):最早入表的那一条是**最久没有收口**的,它要么已经因为
34
+ * 上面两种形态永远不会收口,要么它的消费窗(收口当拍)早就过去了 —— 丢它的代价最小。绝不按
35
+ * 「随便丢一个」或整表清空(整表清空会把同批在飞的兄弟一起打掉)。
36
+ * 🔴 **淘汰打到仍在飞的 call 时会怎样(成文的取舍,不是漏)**:那只 call 后来被分类器拒绝的话,
37
+ * 宿主拿到的 `ctx` 只有 `toolCallId`、没有 `args` —— 即**诚实缺席**,端按可选处理照常降级渲
38
+ * (契约见 `docs/INTEGRATION-CLIENTS.md` §15c 第 3 条)。这条路上唯一的替代品是「不淘汰」,
39
+ * 而那就是无界表。durable 重放会把 `tool_start` 再送一遍,那是被淘汰项**拿回快照**的机会 ——
40
+ * 所以 `frameRouter.routeToolStart` 把本动词排在渲染去重闸**之前**(见该站点注)。
41
+ */
42
+ export const START_ARGS_MAX_ENTRIES = 256;
12
43
  /** 造一份 turn 级 gate 台账(**不是单例**,见文件头注)。 */
13
44
  export function createGateLedger() {
14
45
  const startedCalls = new Set();
@@ -16,6 +47,8 @@ export function createGateLedger() {
16
47
  /** 只存帧:出身不在入表当拍冻结(那一拍 gate 身份还没到),改在 `flushHeld` 当拍求值。 */
17
48
  const heldAskEnds = new Map();
18
49
  const gatedStartArgsByCall = new Map();
50
+ /** 每一张 tool_start 的 args 快照(在飞窗;有界 FIFO,见 {@link START_ARGS_MAX_ENTRIES})。 */
51
+ const startArgsByCall = new Map();
19
52
  const deniedCalls = new Set();
20
53
  const pendingFsCalls = [];
21
54
  const resolvedAnswers = new Map();
@@ -140,6 +173,27 @@ export function createGateLedger() {
140
173
  gatedStartArgs() {
141
174
  return gatedStartArgsByCall;
142
175
  },
176
+ noteStart(callId, ev) {
177
+ // 🔴 **先删再写**:同一 callId 重放(durable 重放的 tool_start 在别的入口被 markStarted 挡掉,
178
+ // 但本动词不依赖那道闸)必须刷新它在 FIFO 里的位置,否则「覆盖」会让它保持旧的插入序、
179
+ // 在还活着的时候被当成最老的一条淘汰掉。`Map.set` 对已有键**不**改插入序,这一行是承重的。
180
+ startArgsByCall.delete(callId);
181
+ startArgsByCall.set(callId, ev.args);
182
+ // 有界:超容量丢**最早**入表的那一条(`Map` 迭代序 = 插入序)。while 而不是 if —— 上限被
183
+ // 调小时(或将来某次批量写入)一次要淘汰多条,if 会让表永久停在超限状态。
184
+ while (startArgsByCall.size > START_ARGS_MAX_ENTRIES) {
185
+ const oldest = startArgsByCall.keys().next();
186
+ if (oldest.done === true)
187
+ break;
188
+ startArgsByCall.delete(oldest.value);
189
+ }
190
+ },
191
+ startArgs() {
192
+ return startArgsByCall;
193
+ },
194
+ dropStartArgs(callId) {
195
+ startArgsByCall.delete(callId);
196
+ },
143
197
  isEnded(callId) {
144
198
  return endedCalls.has(callId);
145
199
  },
@@ -7,14 +7,41 @@ export interface HitlNotice {
7
7
  priority: 'immediate';
8
8
  timeoutMs: number;
9
9
  }
10
+ /**
11
+ * 分类器 deny 裁决的**这一只 call 的现场**(L-69⑨,0.52.0;{@link HitlHostSurface.surfaceClassifierDeny}
12
+ * 的 **additive 可选**第三参)。
13
+ *
14
+ * 🔴 为什么要有它:修前宿主只拿到 `toolName`,于是端的 Recent Denials 面板三行同名(三条被拒的
15
+ * `Bash` 长一个样),而端要做的两件事 —— **行文案**(这一行说的是哪条命令 / 哪个文件)与
16
+ * **retry 粒度**(重试这一只调用,而不是「整个 Bash 工具」)—— 都需要这一只 call 的身份与入参。
17
+ * 🔴 `args` 是 **UNTRUSTED** wire 值(模型产出的工具入参):本包**原样**交给宿主,不渲染、不截断、
18
+ * 不消毒 —— 展示消毒是端的事(cli 有 `cleanUntrustedForDisplay`)。要拿它铸**行文案**请用
19
+ * `classifierDenyDisplay(toolName, args)`(`src/classifierVerdictWire.ts`,三端同形的判定)。
20
+ * 🔴 `args` **可缺席**:分类器拒绝的工具通常没被 gate,而在飞 args 表是**有界**的(见
21
+ * `gateLedger.START_ARGS_MAX_ENTRIES`),长流上早期 call 的快照可能已被淘汰。缺席 = 诚实缺席,
22
+ * 端按「只有工具名」降级渲,**绝不**据此编一个空 args 出来。
23
+ */
24
+ export interface ClassifierDenyContext {
25
+ /** 这一只被拒调用的 wire `toolCallId`(retry 粒度的锚;帧上取,恒在场)。 */
26
+ toolCallId: string;
27
+ /** 这一只调用的 `tool_start` 入参快照;**UNTRUSTED**,缺席 = 台账里没有(见上)。 */
28
+ args?: unknown;
29
+ }
10
30
  /** HITL 的宿主副作用面 —— 通知上屏 + 分类器 deny 的端侧记账。 */
11
31
  export interface HitlHostSurface {
12
32
  /** 立即顶到 current(壳:`notifications.current = notice`,queue 不动)。 */
13
33
  showNotice(notice: HitlNotice): void;
14
34
  /** **仅当** current 仍是这个 key 时清掉(壳原文的 if-still-mine 语义 —— 否则会误清别人的通知)。 */
15
35
  clearNoticeIfCurrent(key: string): void;
16
- /** 分类器 deny 裁决的宿主副作用:footer 通知 + /permissions Recent Denials 记账(壳资产)。 */
17
- surfaceClassifierDeny(toolName: string, verdict: ClassifierDenyVerdict): void;
36
+ /**
37
+ * 分类器 deny 裁决的宿主副作用:footer 通知 + /permissions Recent Denials 记账(壳资产)
38
+ *
39
+ * 🔴 `ctx` 是 **additive 可选**第三参(0.52.0,L-69⑨):老宿主的两参实现**零改动照跑**
40
+ * —— JS 里多传一个实参不影响两参函数,TS 里「参数少的函数可赋给参数多的签名」是语言规则
41
+ * (函数参数双变/协变),desktop/web 现有 `surfaceClassifierDeny(t, v)` 实现不需要跟车。
42
+ * ⚠️ 反过来**不成立**:端一旦读了 `ctx`,就必须按可选处理(库对老路径/缺席场景不承诺在场)。
43
+ */
44
+ surfaceClassifierDeny(toolName: string, verdict: ClassifierDenyVerdict, ctx?: ClassifierDenyContext): void;
18
45
  }
19
46
  /** 装 HITL 宿主面(传 null 卸)。返回还原函数。 */
20
47
  export declare function installHitlHostSurface(surface: HitlHostSurface | null): () => void;
@@ -17,6 +17,32 @@ import { type GateCurrentPending } from './hitlBridge.js';
17
17
  import { type QuestionAnswer } from '../liveQuestionStore.js';
18
18
  import type { AskAnsweredOutput, GateLedger } from './gateLedger.js';
19
19
  import { type AskGateWireDeps, type GatePark } from './frameRouter.js';
20
+ /**
21
+ * L-80(2026-09-03,[6215];#357 复发):**连续非进展轮**上限 —— 数的是「壳再附着之后那段流里一个
22
+ * host 推进帧都没有」的轮次,**不是** park 次数。修前这里是 `24` 且按每次 park 递增
23
+ * (askGateWire `hops++` 无条件),于是一个 turn 里模型对同一失败 Edit 重试 24 次、每次真被门真被决,
24
+ * 也会在第 25 次撞成 `gate hop limit (24) exceeded`——一句与真因无关的预算话术。CC 没有这条预算:
25
+ * 模型问 N 次就答 N 次。本闸只留作最后防线:连续 N 轮零进展(坐标失配空转 / reopen 类 re-park /
26
+ * decide 出站恒断)才收场,收场前再呈一次收场卡、终帧带真因与出路。
27
+ */
28
+ export declare const MAX_GATE_HOPS = 2;
29
+ /** 一轮 park 的进展判决(驱动侧按 `isHostProgressFrame` 观察,见 askGateWire)。 */
30
+ export type HopRound = 'progress' | 'stalled';
31
+ /** hop 预算状态:`stalled` = 连续非进展轮数(进展即清零);`total` = 本 turn park 总数(只用于日志)。 */
32
+ export interface HopBudget {
33
+ readonly stalled: number;
34
+ readonly total: number;
35
+ }
36
+ /** 纯函数:上一轮预算 + 本轮判决 ⇒ 新预算(不就地改入参;`undefined` = 本 turn 第一轮)。 */
37
+ export declare function nextHopBudget(prev: HopBudget | undefined, round: HopRound): HopBudget;
38
+ /**
39
+ * 硬兜底(对抗复审 r1 [high]①,r6 [high] 收窄):一 turn 内**解析不出身份**的 park 数上限。连续非进展计数靠
40
+ * 「解析到的 pending 行身份 + host 推进帧」判进展;解析不到行(tool-less park / approvals 恒空)时身份判不出 ⇒
41
+ * 只能靠推进帧,一个每轮先吐一帧文本再原样 park 的病态引擎在那一形下仍可能无限。**只数这一类**:每轮都解析到
42
+ * 新 call 且真推进的门(模型重试同一失败调用 N 次)不吃这格 —— CC 没有此闸,问 65 次就答 65 次。
43
+ * 命中即收场、终帧如实说「本 turn 有 N 次 park 解析不出身份」。
44
+ */
45
+ export declare const MAX_TOTAL_PARKS = 64;
20
46
  /**
21
47
  * REF-CC-033:`GateOutcome` 的 `failed` 分支闭集判别码。来源两处:①本文件自己的
22
48
  * 「pending 行取不到」早退(`surfaceGateAndDecide` 里 `!pending` 那条);②`HitlBridge` 抛出的
@@ -69,8 +95,19 @@ export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
69
95
  * (multiSelect 与对话框同款 ", " lossy join;note → annotations.notes)。 */
70
96
  export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
71
97
  /** 外环决断的两种出路(见文件头注)。 */
98
+ /** resolvePark 的续流判决:`progress` 位 = 这一轮决断真落地了一次(驱动侧不据此复位计数 —— 复位只看
99
+ * 下一段流有没有 host 推进帧);非进展轮携 `reason`(收场文案用)。 */
72
100
  export type ParkResolution = {
73
101
  kind: 'reattach';
102
+ progress: true;
103
+ presented: boolean;
104
+ gatedCallId?: string | undefined;
105
+ } | {
106
+ kind: 'reattach';
107
+ progress: false;
108
+ reason: string;
109
+ presented: boolean;
110
+ gatedCallId?: string | undefined;
74
111
  } | {
75
112
  kind: 'failsoft';
76
113
  events: readonly AgentEvent[];
@@ -80,8 +117,26 @@ export interface ParkResolveCtx {
80
117
  led: GateLedger;
81
118
  /** sync leg 捕获的真 run handle(内环 `TaskIdRef` 的当前值)。 */
82
119
  taskId: string;
83
- /** 本 turn 内这是第几次 park(hop 预算的唯一计量;由驱动累加,本函数只读)。 */
120
+ /** 本 turn park 总数(日志用;**不再**是预算计量,见 {@link MAX_GATE_HOPS})。 */
84
121
  hops: number;
122
+ /** 本 turn 内**解析不出身份**的 park 数(驱动侧累计;超过 {@link MAX_TOTAL_PARKS} 即硬兜底收场)。 */
123
+ unverifiableParks?: number;
124
+ /** 连续非进展轮数(驱动侧 {@link nextHopBudget} 算出;超过 {@link MAX_GATE_HOPS} 即收场)。 */
125
+ stalledRounds?: number;
126
+ /** 上一轮非进展的真因 token(收场文案用)。 */
127
+ lastStallReason?: string | undefined;
128
+ /** 上一轮有没有真呈过卡(收场卡只在**没呈过**时才呈 —— 呈过再呈 = 把同一张失效卡再问一遍,F15-e② 禁)。 */
129
+ lastRoundPresentedCard?: boolean | undefined;
85
130
  signal?: AbortSignal | undefined;
86
131
  }
87
132
  export declare function resolvePark(park: GatePark, ctx: ParkResolveCtx): Promise<ParkResolution>;
133
+ /**
134
+ * 驱动侧在**预算提交之后**判定触顶时的收场(对抗复审 r4 [high]①:有推进帧的轮次触顶必须等权威身份解析完 ——
135
+ * 先呈 B、决 B,提交后仍是同一只 call 才收场;这一轮已经呈过卡,不再呈收场卡)。
136
+ */
137
+ export declare function stalledTerminal(park: GatePark, ctx: ParkResolveCtx, stalledRounds: number, lastStallReason: string | undefined,
138
+ /** 这一轮的决断有没有落地(r5 [medium]③):落地了就**不许**说「could not be answered」「still parked」—— 那是谎报。 */
139
+ decidedThisRound: boolean,
140
+ /** 这一轮决断落在哪只 call(解析到的行身份):收场排水前先把**它**的扣留帧摘掉(r6 [medium]:两只同族 call 都在
141
+ * 扣留、park 帧无可信身份时,决断臂刻意保守不摘,排水会把刚批准的那只当 abort 吐出)。 */
142
+ decidedGatedCallId?: string | undefined): ParkResolution;