@sema-agent/client-core 0.46.0 → 0.48.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 +188 -0
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/dist/adapt/arms.js +10 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +96 -26
- package/dist/hitl/gateLedger.d.ts +21 -0
- package/dist/hitl/gateLedger.js +17 -0
- package/dist/hitl/hitlBridge.d.ts +61 -1
- package/dist/hitl/hitlBridge.js +49 -0
- package/dist/hitl/parkResolver.d.ts +2 -0
- package/dist/hitl/parkResolver.js +192 -30
- package/dist/hitl/toolApprovalWire.d.ts +36 -2
- package/dist/hitl/toolApprovalWire.js +29 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/interactiveHalt.d.ts +150 -0
- package/dist/interactiveHalt.js +131 -0
- package/dist/retryStatus.d.ts +50 -3
- package/dist/retryStatus.js +31 -7
- package/dist/sessionMemoryStatus.d.ts +138 -0
- package/dist/sessionMemoryStatus.js +181 -0
- package/dist/toolResult.js +9 -0
- package/dist/typePins.d.ts +40 -0
- package/docs/INTEGRATION-CLIENTS.md +353 -26
- package/package.json +6 -6
- package/docs/REFACTOR-LEDGER.md +0 -392
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
* vocabulary, no claude.ai permission-rule destinations, no widget/glyph/keybinding crosses this seam. The
|
|
66
66
|
* backend supplies the SIGNAL; the shell owns the chrome.
|
|
67
67
|
*/
|
|
68
|
-
import type { AgentEvent, ApprovalDecision, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
|
|
68
|
+
import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
|
|
69
69
|
/** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
|
|
70
70
|
export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
|
|
71
71
|
/** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
|
|
@@ -155,6 +155,66 @@ 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
|
+
/**
|
|
159
|
+
* decide 409 `approval_stale` 拒体上的 **additive 指路键** `currentPending` —— 本会话**当前**那条
|
|
160
|
+
* pending 的三件 D-1 坐标(#363,0.47.0;server S-02 ≥7.52)。
|
|
161
|
+
*
|
|
162
|
+
* ── 它是什么、不是什么 ────────────────────────────────────────────────────────────────────
|
|
163
|
+
* 是:「你手上那条已经不是当前的了,**当前**这条长这样」——壳拿它**一跳重定位**再 decide,
|
|
164
|
+
* 免去整队重拉(`GET /v1/approvals`)。
|
|
165
|
+
* 不是:放行凭据 / CAS 输入 / resume 凭证。server 自己的 fail-open 登记原话逐字:
|
|
166
|
+
* 「纯指路/便利面,**不参与任何门/CAS/resume 判定**」。⇒ 缺席的代价 = 退回它到货前的行为
|
|
167
|
+
* (重拉列表自行重定位),所以本包对它一律**宽读、诚实缺席**,绝不 fail-closed。
|
|
168
|
+
* 🔴 **恒不含 `checkpointToken`**(server 侧 resume 凭证不外发纪律)——本形也刻意没有那一位:
|
|
169
|
+
* 多一个位就是给一个「凭证能过境」的错觉。
|
|
170
|
+
*
|
|
171
|
+
* ── 自铸形已退役,本名现在是上游形的**别名**(0.48.0)──────────────────────────────────
|
|
172
|
+
* 0.47.0 这里是一个**自铸 interface** + 一段记账:`@sema-agent/sdk` 7.3.0 起已有逐字同形的
|
|
173
|
+
* `ApprovalStaleCurrentPending`,但当时本包 peer 地板是 `>=7.2.0`,那一版上这个名字不存在 ⇒
|
|
174
|
+
* `import type` 会让装 7.2.0 的端当场编不过。记账段同批写死了退役条件:「peer 地板抬到 `>=7.3.0`
|
|
175
|
+
* 的那一批,把本形改成 `import type` 的别名并删掉这段记账」。
|
|
176
|
+
* 本批(0.48.0)peer 地板抬到 **`>=7.4.0`**(四件新投影都吃 7.4.0 的类型面)⇒ 条件满足,兑现。
|
|
177
|
+
* 🔴 **别名而不是删名**:`GateCurrentPending` 是 `FsApprovalOutcome` / `GateOutcome` 两条腿的**已发布
|
|
178
|
+
* 导出名**,删它是 BREAKING。别名让上游形成为**唯一真源**(上游改一个位,本包的两条腿当天跟着
|
|
179
|
+
* 动,不再有第二份可能漂的声明),而本名继续对已接入的端有效。
|
|
180
|
+
* 🔴 **同名不同源的风险随之消失**:0.47.0 刻意取了个**不同名**(免得地板线下出现一个与上游同名却
|
|
181
|
+
* 不同源的形,[same-name-different-meaning-crosses-layers]);现在两者是同一个形,那条顾虑不再
|
|
182
|
+
* 适用 —— 但名字**不回改**成上游名,理由见上一条(已发布导出名)。
|
|
183
|
+
*
|
|
184
|
+
* 🔴 **为什么绕道 `ApprovalStaleError['currentPending']` 而不是直接 `import type
|
|
185
|
+
* { ApprovalStaleCurrentPending }`(上游缺口,本批亲验实证)**:那个名字在 sdk 7.4.0 里**声明了
|
|
186
|
+
* 但没有从包入口导出** —— `dist/types.d.ts:2359` 有 `export interface ApprovalStaleCurrentPending`,
|
|
187
|
+
* 而 `dist/index.d.ts` 的 re-export 名单里**没有它**(同文件只导出了 `ApprovalStaleError`),
|
|
188
|
+
* 且包 `exports` 映射只开 `.` / `./registry` 两个子路径,深引 `@sema-agent/sdk/dist/types.js`
|
|
189
|
+
* 这条路根本不通。⇒ 按名 import 是**编译错**,不是风格选择。
|
|
190
|
+
* 绕道取的是**同一个声明**(`currentPending?: ApprovalStaleCurrentPending | undefined`,
|
|
191
|
+
* `NonNullable` 剥掉可选位),所以单源化的目的一字不打折:上游改那个形,本包当天跟着动。
|
|
192
|
+
* 📋 **上游候件(已登记,不在本批修)**:请 sdk 把 `ApprovalStaleCurrentPending` 加进 `index.d.ts`
|
|
193
|
+
* 的 `export type {…}` 名单(纯 additive,零行为)。到货后本别名可改回按名 import,
|
|
194
|
+
* **但那是等价重构、不是修复** —— 今天这条路已经拿到上游形本身,不存在第二份可能漂的声明。
|
|
195
|
+
*/
|
|
196
|
+
export type GateCurrentPending = Readonly<NonNullable<ApprovalStaleError['currentPending']>>;
|
|
197
|
+
/**
|
|
198
|
+
* 从一次 decide 失败(任意抛出物)里读出 {@link GateCurrentPending} —— 读不出即 `undefined`。
|
|
199
|
+
*
|
|
200
|
+
* 🔴 **结构视图读,不 `instanceof`**(两条理由,都是承重的):
|
|
201
|
+
* ① **供给面**:本包的 decide 出站走的是宿主注入的 `HitlClientLike`(见该型)。抛出来的到底是不是
|
|
202
|
+
* SDK 的 `ApprovalStaleError` 由**宿主**决定 —— web/desktop 完全可以注入自己的传输层。拿
|
|
203
|
+
* `instanceof` 当闸,等于让「宿主用的是哪一份 SDK 实例」决定一个纯读值的位在不在
|
|
204
|
+
* (跨 realm / 双实例 = `HitlSafetyError` 判型契约踩过的同一个坑,见 §4e)。
|
|
205
|
+
* ② **类型面**(0.48.0 就地订正):0.47.0 这条写的是「`ApprovalStaleError.currentPending` 只在
|
|
206
|
+
* sdk ≥7.3.0 上声明,而本包地板是 7.2.0」——**地板已抬到 `>=7.4.0`,那半条不再成立**。
|
|
207
|
+
* 但本条的**结论不变**,因为承重的从来是 ① 那半:即使类型面现在够得着,抛出来的对象仍由宿主
|
|
208
|
+
* 的传输层决定,`instanceof` 依旧是错的判据。⇒ 保留宽读,只订正理由。
|
|
209
|
+
* 🔴 **判据锚在「值本身合不合形」,不锚它骑在哪个类/哪个 errorCode 上**
|
|
210
|
+
* ([anchor-on-the-deciding-quantity]):真正决定「壳能不能一跳重定位」的量,是**这个三元组的
|
|
211
|
+
* 两件必填位在不在且是非空串** —— 而不是错误的类名。宽读在这里是**方向安全**的:本位不参与
|
|
212
|
+
* 任何裁决(见 {@link GateCurrentPending} 顶注),多读到一次的代价 = 多一条指路;漏读的代价 =
|
|
213
|
+
* 退回整队重拉。
|
|
214
|
+
* 🔴 **两件必填位缺一即整只不铸**:半个三元组重定位不了(`decide` 要的是 boundCallId 本体),
|
|
215
|
+
* 铸一个残形只会让消费点以为「有指路」。`boundInputHash` 是**行上有才带**的可选位,缺席照铸。
|
|
216
|
+
*/
|
|
217
|
+
export declare function readDecideCurrentPending(e: unknown): GateCurrentPending | undefined;
|
|
158
218
|
/** 测试钩:把超时类重试总窗调小(传 undefined 还原缺省)。 */
|
|
159
219
|
export declare function __setDecideTimeoutRetryBudgetForTests(ms?: number): void;
|
|
160
220
|
/**
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -53,6 +53,55 @@ export class HitlSafetyError extends Error {
|
|
|
53
53
|
this.name = 'HitlSafetyError';
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 🔴 **编译期负控**(对抗复审 [medium] 采纳,0.48.0):三位必须**全 `readonly`**。
|
|
58
|
+
*
|
|
59
|
+
* 0.47.0 的自铸形三位逐个写着 `readonly`,而上游 `ApprovalStaleCurrentPending` 的成员是**可写**的
|
|
60
|
+
* —— 首版别名(`NonNullable<…>`,不带 `Readonly<>`)把这条已发布的类型契约悄悄弱了一档:
|
|
61
|
+
* 消费方从此可以直接改写 `boundCallId` / `boundInputHash`,而那两位的契约恰恰是
|
|
62
|
+
* **逐字回显、绝不本地重算**(§9.1;本地重算一次就会把一次合法审批 fail-close 掉)。
|
|
63
|
+
* 🔴 这类退化**没有任何行为门看得见**(它不改变任何运行期字节)—— 名字在、字段在、行为门全绿。
|
|
64
|
+
* 所以钉必须下在编译期:`Readonly<>` 哪天被顺手摘掉,这一行当场编译错。
|
|
65
|
+
*/
|
|
66
|
+
const _gateCurrentPendingReadonlyPin = true;
|
|
67
|
+
void _gateCurrentPendingReadonlyPin;
|
|
68
|
+
/**
|
|
69
|
+
* 从一次 decide 失败(任意抛出物)里读出 {@link GateCurrentPending} —— 读不出即 `undefined`。
|
|
70
|
+
*
|
|
71
|
+
* 🔴 **结构视图读,不 `instanceof`**(两条理由,都是承重的):
|
|
72
|
+
* ① **供给面**:本包的 decide 出站走的是宿主注入的 `HitlClientLike`(见该型)。抛出来的到底是不是
|
|
73
|
+
* SDK 的 `ApprovalStaleError` 由**宿主**决定 —— web/desktop 完全可以注入自己的传输层。拿
|
|
74
|
+
* `instanceof` 当闸,等于让「宿主用的是哪一份 SDK 实例」决定一个纯读值的位在不在
|
|
75
|
+
* (跨 realm / 双实例 = `HitlSafetyError` 判型契约踩过的同一个坑,见 §4e)。
|
|
76
|
+
* ② **类型面**(0.48.0 就地订正):0.47.0 这条写的是「`ApprovalStaleError.currentPending` 只在
|
|
77
|
+
* sdk ≥7.3.0 上声明,而本包地板是 7.2.0」——**地板已抬到 `>=7.4.0`,那半条不再成立**。
|
|
78
|
+
* 但本条的**结论不变**,因为承重的从来是 ① 那半:即使类型面现在够得着,抛出来的对象仍由宿主
|
|
79
|
+
* 的传输层决定,`instanceof` 依旧是错的判据。⇒ 保留宽读,只订正理由。
|
|
80
|
+
* 🔴 **判据锚在「值本身合不合形」,不锚它骑在哪个类/哪个 errorCode 上**
|
|
81
|
+
* ([anchor-on-the-deciding-quantity]):真正决定「壳能不能一跳重定位」的量,是**这个三元组的
|
|
82
|
+
* 两件必填位在不在且是非空串** —— 而不是错误的类名。宽读在这里是**方向安全**的:本位不参与
|
|
83
|
+
* 任何裁决(见 {@link GateCurrentPending} 顶注),多读到一次的代价 = 多一条指路;漏读的代价 =
|
|
84
|
+
* 退回整队重拉。
|
|
85
|
+
* 🔴 **两件必填位缺一即整只不铸**:半个三元组重定位不了(`decide` 要的是 boundCallId 本体),
|
|
86
|
+
* 铸一个残形只会让消费点以为「有指路」。`boundInputHash` 是**行上有才带**的可选位,缺席照铸。
|
|
87
|
+
*/
|
|
88
|
+
export function readDecideCurrentPending(e) {
|
|
89
|
+
const raw = e?.currentPending;
|
|
90
|
+
if (typeof raw !== 'object' || raw === null)
|
|
91
|
+
return undefined;
|
|
92
|
+
const o = raw;
|
|
93
|
+
if (typeof o.toolName !== 'string' || o.toolName.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (typeof o.boundCallId !== 'string' || o.boundCallId.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
return {
|
|
98
|
+
toolName: o.toolName,
|
|
99
|
+
boundCallId: o.boundCallId,
|
|
100
|
+
...(typeof o.boundInputHash === 'string' && o.boundInputHash.length > 0
|
|
101
|
+
? { boundInputHash: o.boundInputHash }
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
56
105
|
// ── decide 出站的瞬断分诊与有界重试(Inkglow-1085 P0a,2026-08-19;[4664] server 定谳后对表)────
|
|
57
106
|
//
|
|
58
107
|
// 病形(案卷 INKGLOW-1085 车1 全链):`approvals.decide` 是**非 submit POST** —— SDK transport 对它
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 在驱动的消费点上必然显形,而不是被本文件自己吞掉。
|
|
14
14
|
*/
|
|
15
15
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
16
|
+
import { type GateCurrentPending } from './hitlBridge.js';
|
|
16
17
|
import { type QuestionAnswer } from '../liveQuestionStore.js';
|
|
17
18
|
import type { AskAnsweredOutput, GateLedger } from './gateLedger.js';
|
|
18
19
|
import { type AskGateWireDeps, type GatePark } from './frameRouter.js';
|
|
@@ -46,6 +47,7 @@ export type GateOutcome = {
|
|
|
46
47
|
reason: string;
|
|
47
48
|
code?: GateFailureCode | undefined;
|
|
48
49
|
retryExhausted?: true;
|
|
50
|
+
currentPending?: GateCurrentPending | undefined;
|
|
49
51
|
};
|
|
50
52
|
/**
|
|
51
53
|
* decide 的 `failed` 原因是不是「这个 gate 早就被解决了」(#110 缺陷② c 的判据)。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask } from './hitlBridge.js';
|
|
1
|
+
import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, } from './hitlBridge.js';
|
|
2
2
|
import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
|
|
3
3
|
import { hostLog } from '../host.js';
|
|
4
4
|
import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
@@ -7,6 +7,38 @@ import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
|
|
|
7
7
|
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
8
8
|
/** 一 turn 内最多循环这么多次 park(防御:引擎/模型病态连环提问时不无限 attach)。 */
|
|
9
9
|
const MAX_GATE_HOPS = 24;
|
|
10
|
+
/**
|
|
11
|
+
* 「已解决 ⇒ reattach」臂的**同因连续命中上限**(#357,现网 P0;[5522] 定谳 / [5525] server 答复)。
|
|
12
|
+
* 同一判据源连续第 `N+1` 次命中即**不再** reattach。
|
|
13
|
+
*
|
|
14
|
+
* ── 病 ──────────────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
* 用户批完两张卡,第三只门 16 分钟长等,最后吐 `gate hop limit (24) exceeded`,工具 abort,
|
|
16
|
+
* 而引擎侧 run 仍 parked。链:run 仍 parked 但取件当拍读不到本 run 的可决行 ⇒ 每一轮重放的 park
|
|
17
|
+
* 都命中「已解决」⇒ reattach ⇒ 引擎重放同一张 park ⇒ 循环到 hop 预算耗尽。24 轮里只有 debug 级
|
|
18
|
+
* 日志,用户零告知,拿到的还是一句与真因无关的预算话术。
|
|
19
|
+
*
|
|
20
|
+
* ── 两件修的分工(**别合并**)──────────────────────────────────────────────────────────────
|
|
21
|
+
* · **主修 = 坐标重探**({@link resolvePark} 的 rescan 臂):server [5525] 定谳这不是「状态失配」
|
|
22
|
+
* 而是**坐标失配** —— decide 受理后 resume **消费**掉 checkpoint(token 一次性),下一 turn 再
|
|
23
|
+
* 撞门就是**新 park / 新 token / 新行**;拿旧坐标去查/去决恒得 409 `approval_stale`(体
|
|
24
|
+
* terminal:"resolved")或 404 no-pending,逐字命中本文件的「已解决」判据集,而 run 真身是
|
|
25
|
+
* parked、**新行真实存在待决**。⇒ 命中「已解决」时先**重读当前队列**(丢掉这张 park 自带的
|
|
26
|
+
* 陈旧身份,让取件腿按当前拍的坐标选行):有行 ⇒ 这一轮就把**新卡**真呈给用户;确无行 ⇒ 才是
|
|
27
|
+
* #110 的重放窗,照旧 reattach。
|
|
28
|
+
* · **兜底 = 本闸**:重探自己也失配/读不动时(网络类、或队列确实空而 run 就是不动),不许再
|
|
29
|
+
* 空转 24 轮 —— 同因连续第 2 次即诚实收场,终帧带真因。
|
|
30
|
+
*
|
|
31
|
+
* 值 = 1 的理由:「已解决」判据的**正当性只覆盖一次重放窗**(#110 成文:`lastEventId` 再准也只能
|
|
32
|
+
* 精确到帧,park 就在重放窗里,而那个窗**只有一次**)。第二次同因命中意味着重探也没能把新坐标
|
|
33
|
+
* 找出来 —— 那时继续 reattach 只是把同一个空转再跑一遍。
|
|
34
|
+
* 🔴 {@link MAX_GATE_HOPS} 总闸**保留**(最后防线):本闸只管「已解决」这一条臂,别的臂
|
|
35
|
+
* (重呈臂每轮真呈卡)仍由总闸兜底。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 键**按语义分不按表征分**(异源复审 finding③):「已解决」在 wire 上有 `no_pending` 码 /
|
|
38
|
+
* 409 体 `resolved` / 404 `not found` 三种长相,它们是同一个真因 —— 按长相分键的话,表征一轮换
|
|
39
|
+
* 计数就重置回 1,闸永远不触发。判据见 {@link alreadyResolvedStreakKey}。
|
|
40
|
+
*/
|
|
41
|
+
const MAX_ALREADY_RESOLVED_REATTACHES = 1;
|
|
10
42
|
/**
|
|
11
43
|
* REF-CC-033:`GateOutcome` 的 `failed` 分支闭集判别码。来源两处:①本文件自己的
|
|
12
44
|
* 「pending 行取不到」早退(`surfaceGateAndDecide` 里 `!pending` 那条);②`HitlBridge` 抛出的
|
|
@@ -51,13 +83,28 @@ function isGateFailureCode(v) {
|
|
|
51
83
|
* (desktop 实测——parked 刚批完、decide 未返回的竞态窗内 Stop 走 parked-deny 路),旧词表
|
|
52
84
|
* 判不中 ⇒ 按真失败 log-only 放弃 ⇒ runs.cancel 永不发出=Stop 哑弹。按上方「宁可宽」方向收编。 */
|
|
53
85
|
export function isAlreadyResolvedGateReason(reason) {
|
|
86
|
+
return alreadyResolvedReasonToken(reason) !== undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 文案兜底臂的**词表**(判据本体;`isAlreadyResolvedGateReason` 是它的布尔投影)。
|
|
90
|
+
* 🔴 #357 把它从「四个 `||` 就地写死」抽成词表,**行为一个字节不变**:一处词表,两个消费点
|
|
91
|
+
* (布尔判定 + 诊断串里「是哪个词命中的」),此前后者只能靠人再读一遍那四个 `||`。
|
|
92
|
+
* 🔴 命中的词**不进限次闸的键** —— 键按语义分,理由见 {@link alreadyResolvedStreakKey}。
|
|
93
|
+
*/
|
|
94
|
+
const ALREADY_RESOLVED_REASON_TOKENS = ['no pending checkpoint', 'resolved', 'already', 'not found'];
|
|
95
|
+
/** 命中的那个词(没命中 ⇒ `undefined`)。顺序即优先级,只进诊断串,不参与判定强弱。 */
|
|
96
|
+
function alreadyResolvedReasonToken(reason) {
|
|
54
97
|
if (typeof reason !== 'string')
|
|
55
|
-
return
|
|
98
|
+
return undefined;
|
|
56
99
|
const r = reason.toLowerCase();
|
|
57
|
-
return
|
|
100
|
+
return ALREADY_RESOLVED_REASON_TOKENS.find((t) => r.includes(t));
|
|
58
101
|
}
|
|
59
|
-
/** 已解决判据(REF-CC-033):code 在场就只信 code
|
|
60
|
-
* `FsApprovalOutcome` 今天没有
|
|
102
|
+
/** 已解决判据(REF-CC-033):code 在场就只信 code,不在场才落回文案兜底。
|
|
103
|
+
* ✅ **记案已销**(#357,0.47.0):原注末句写的是「fs 审批腿的 `FsApprovalOutcome` 今天没有
|
|
104
|
+
* code 字段——C-bridge 卡域,退役条件 = 那边也补上 code」。那个退役条件已兑现:取件腿的
|
|
105
|
+
* `no_pending` 码现在由 `toolApprovalWire.surfaceFsApprovalAndDecide` 原样透传(见
|
|
106
|
+
* `FsApprovalOutcome.code` 头注),两条腿从此同走 code 臂;文案兜底只剩 decide 失败那一类
|
|
107
|
+
* (引擎的 409/404 语义答复文本)与旧引擎路径。
|
|
61
108
|
* 🔴 入参**已经**是窄化到 `{kind:'failed'}` 的那半:一个「true ⇒ 是 failed」的判定不能反过来
|
|
62
109
|
* 当「false ⇒ 不是 failed」的类型谓词用(TS 的 `outcome is Extract<…>` 负向窄化会把整个
|
|
63
110
|
* `'failed'` 分支从联合类型里排除,而这个函数对未解决的真失败照样返回 false——那不是「它不是
|
|
@@ -196,6 +243,8 @@ parkGatedCallId) {
|
|
|
196
243
|
// 这里 instanceof 窄化后再过 isGateFailureCode 白名单——不认得的码(将来 hitlBridge 那边
|
|
197
244
|
// 万一多加一种)一律降级成 undefined,退回文案兜底,不让开集腐蚀这个新判别位。
|
|
198
245
|
const code = e instanceof HitlSafetyError && isGateFailureCode(e.code) ? e.code : undefined;
|
|
246
|
+
// #363:S-02 指路键原样透传 —— 与 `toolApprovalWire` 两条 decide 失败腿同形同源。
|
|
247
|
+
const currentPending = readDecideCurrentPending(e);
|
|
199
248
|
return {
|
|
200
249
|
kind: 'failed',
|
|
201
250
|
gatedCallId,
|
|
@@ -203,6 +252,7 @@ parkGatedCallId) {
|
|
|
203
252
|
...(code !== undefined ? { code } : {}),
|
|
204
253
|
// Inkglow-1085 P0a:ask 腿与 fs 腿同形 —— 瞬断耗尽给判别位,resolvePark 走重呈臂。
|
|
205
254
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
255
|
+
...(currentPending !== undefined ? { currentPending } : {}),
|
|
206
256
|
};
|
|
207
257
|
}
|
|
208
258
|
}
|
|
@@ -210,22 +260,55 @@ parkGatedCallId) {
|
|
|
210
260
|
function argsByCallOf(led) {
|
|
211
261
|
return new Map(led.gatedStartArgs());
|
|
212
262
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
/**
|
|
264
|
+
* 限次闸的**判据源**(键)——「是哪一**条判据**把这一轮判成已解决的」。
|
|
265
|
+
*
|
|
266
|
+
* 🔴 键**按语义分,不按表征分**(异源复审 finding③ 的采纳):「队列里没有本 run 的可决行」这件事
|
|
267
|
+
* 在 wire 上有好几种长相 —— 取件读到空表落 `code:'no_pending'`,server 的 409 `approval_stale`
|
|
268
|
+
* (体 terminal:"resolved")/ 404 落文案臂的 `resolved` / `not found`。它们是**同一个真因的三种
|
|
269
|
+
* 说法**;按长相分键的话,一个卡死的 gate 只要在这几种表征之间轮换,连续计数就一路重置回 1,
|
|
270
|
+
* 闸永远不触发 —— 正是本闸要消灭的那个空转([anchor-on-the-deciding-quantity]:锚真正决定结果
|
|
271
|
+
* 的量,不锚它的表征)。命中的那个词只进诊断串,不进键。
|
|
272
|
+
*
|
|
273
|
+
* 只有身份腿(`decidedGates` 一次性匹配)另占一个键:它与「已解决」是两条独立判据,而且它一次性
|
|
274
|
+
* 消费、连续最多命中一次,天然不会把闸推到跳闸线。
|
|
275
|
+
*/
|
|
276
|
+
function alreadyResolvedStreakKey(byDecidedIdOnly) {
|
|
277
|
+
return byDecidedIdOnly ? 'decided-id-match' : 'already-resolved';
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 这一轮的失败是不是「**取件那一步**就没找到本 run 的可决行」—— 即坐标重探唯一有资格跑的那一形。
|
|
281
|
+
*
|
|
282
|
+
* 🔴 判据锚在**结构**上,不锚文案:`code === 'no_pending'`(取件腿的结构化判别位,两条呈卡腿
|
|
283
|
+
* 今天都透传它)**且** `gatedCallId` 缺席。后半条是承重的 —— 两条呈卡腿里,`findPendingForTask`
|
|
284
|
+
* 失败那一条是**唯一**不带 `gatedCallId` 的 failed 出口(行都没选出来,哪来的 call 身份);它之后
|
|
285
|
+
* 的每一个失败出口(卡口缺席 / 无 payload / decide 4xx,包括 `HitlSafetyError.code==='no_pending'`)
|
|
286
|
+
* 都带着 `gatedCallId`。于是这条判据同时证明了一件承重的事:**这一轮没有向用户呈过卡**。
|
|
287
|
+
*
|
|
288
|
+
* 🔴 为什么必须证明「没呈过卡」(异源复审 finding② 的采纳):重探会把整条呈卡腿**再走一遍**。
|
|
289
|
+
* 若允许它在「卡已经呈过、用户答了、decide 回了一句带 resolved 字样的 409」之后跑,用户会在同一轮
|
|
290
|
+
* 里被同一张卡问第二遍(而且可能给出与第一次不同的答案)。把重探限定在取件步失败这一形,
|
|
291
|
+
* 「同一轮重复呈卡」在结构上就不成立。代价:decide 步的 stale 409 不再就地重探 —— 那一形照旧
|
|
292
|
+
* reattach 一次(durable 流本就会把 park 再送一遍),第二次同因命中由限次闸诚实收场。
|
|
293
|
+
*/
|
|
294
|
+
function isFetchStepNoPending(outcome) {
|
|
295
|
+
const code = 'code' in outcome && isGateFailureCode(outcome.code) ? outcome.code : undefined;
|
|
296
|
+
return code === 'no_pending' && outcome.gatedCallId === undefined;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 「呈这张 park 的卡 → 等人决断 → decide」的**唯一**入口(两族各一条腿;fs 腿的 denied 记账随行)。
|
|
300
|
+
*
|
|
301
|
+
* 🔴 #357 抽成一处的理由不是去重:坐标重探臂要用**当前拍的坐标**再走一次同一条腿,两处各写一份
|
|
302
|
+
* 的话,「第一次走的路」与「重探走的路」会各自漂(记账少一笔 / 族判据不同 ⇒ 决断落到另一行)。
|
|
303
|
+
*
|
|
304
|
+
* @param gatedCallId 交给取件腿的**身份**(wire 给的 park 身份;缺席是常态)。首击与坐标重探
|
|
305
|
+
* 传的是**同一个值** —— 重探要的是读数新鲜,不是判据放宽,理由见调用点的红线注。
|
|
306
|
+
*/
|
|
307
|
+
async function surfaceParkGate(park, ctx, gatedCallId) {
|
|
308
|
+
const { deps, led, taskId } = ctx;
|
|
309
|
+
if (park.gate === 'fs') {
|
|
227
310
|
// [816] 放宽腿:fs 写权限 gate → CC 三选卡(vendored PermissionRequest)→ decide。
|
|
228
|
-
outcome = await surfaceFsApprovalAndDecide({ client: deps.client }, taskId, argsByCallOf(led), ctx.signal,
|
|
311
|
+
const outcome = await surfaceFsApprovalAndDecide({ client: deps.client }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
229
312
|
// #51: `outcome`'s declared type is the wider `GateOutcome |
|
|
230
313
|
// FsApprovalOutcome`; both unions share a 'decided' kind with
|
|
231
314
|
// different optional fields (`answered` vs `denied`), so a plain
|
|
@@ -236,9 +319,27 @@ export async function resolvePark(park, ctx) {
|
|
|
236
319
|
if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
|
|
237
320
|
led.markDenied(outcome.gatedCallId); // 重放的报错帧渲 `User rejected …`(见 tool_end 有序臂 ③)
|
|
238
321
|
}
|
|
322
|
+
return outcome;
|
|
323
|
+
}
|
|
324
|
+
return surfaceGateAndDecide(deps, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
325
|
+
}
|
|
326
|
+
export async function resolvePark(park, ctx) {
|
|
327
|
+
// `deps` 不在本函数直读:呈卡/决断两族腿统一经 `surfaceParkGate`(#357 单源化,见其头注)。
|
|
328
|
+
const { led, taskId, hops } = ctx;
|
|
329
|
+
let outcome;
|
|
330
|
+
// REF-CC-034:这次 park 的候选 callId(仅 fs 分支填,已决断身份匹配用)——ask 分支不填,
|
|
331
|
+
// 因为 `surfaceGateAndDecide` 的 `!pending` 早退已经自带 `code:'no_pending'`(REF-CC-033),
|
|
332
|
+
// 不需要这条第二判据。用 `lastFsOrShellGatedCallId()` 而非「未收口 fs call 栈」:durable
|
|
333
|
+
// re-attach 重放的 `suspended` park 没有配套的新 tool_start/tool_end,那个栈在这次重放之前
|
|
334
|
+
// 早被 tool_end 的 drop 清空了(见台账声明处注),取不到候选。
|
|
335
|
+
let candidateGatedCallId;
|
|
336
|
+
if (hops > MAX_GATE_HOPS) {
|
|
337
|
+
outcome = { kind: 'failed', reason: `gate hop limit (${MAX_GATE_HOPS}) exceeded` };
|
|
239
338
|
}
|
|
240
339
|
else {
|
|
241
|
-
|
|
340
|
+
if (park.gate === 'fs')
|
|
341
|
+
candidateGatedCallId = led.lastFsOrShellGatedCallId();
|
|
342
|
+
outcome = await surfaceParkGate(park, ctx, park.gatedCallId);
|
|
242
343
|
}
|
|
243
344
|
// #110 缺陷② b/c —— **重放的、早已决断过的 park 不是失败**。
|
|
244
345
|
// durable re-attach 必然会把 park 帧再送一遍(`lastEventId` 再准也只能精确到帧,park 就在
|
|
@@ -250,10 +351,10 @@ export async function resolvePark(park, ctx) {
|
|
|
250
351
|
// 同 session `--resume` 报 `No conversation found` ⇒ **整个会话废掉**。
|
|
251
352
|
// 无限循环由 `hops > MAX_GATE_HOPS` 兜底(本 reattach 照常吃 hop 预算,不绕过它)。
|
|
252
353
|
//
|
|
253
|
-
// REF-CC-034:`isAlreadyResolvedFailure` 是首选判据(code
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
354
|
+
// REF-CC-034:`isAlreadyResolvedFailure` 是首选判据(code/文案双臂;#357 起 fs 腿的取件码也
|
|
355
|
+
// 走 code 臂,见该函数头注的销案段)。失败原因若两臂都判不出(例如 `approvals.list` 自身网络
|
|
356
|
+
// 失败),已决断身份匹配是独立于文案的第二判据 —— 两臂任一命中都按「已解决」处置,方向偏宽
|
|
357
|
+
// (见台账 `markDecided` 处注)。
|
|
257
358
|
//
|
|
258
359
|
// [2393] hitl-F2(2026-08-02):这条身份判据**一次性消费**(`takeDecided` 而不是 `isDecided`)。
|
|
259
360
|
// 它的正当性只覆盖「durable re-attach 把**那一次**已决断的 park 重放一遍」这一个窗口,而它的两个
|
|
@@ -261,15 +362,73 @@ export async function resolvePark(park, ctx) {
|
|
|
261
362
|
// 不消费就等于:本 turn 只要成功决断过一次,之后**每一次** park 失败都被判「已解决」——
|
|
262
363
|
// `approvals.list` 网络失败这类真失败会连吃 24 个 hop,最后吐一句与真因无关的 `gate hop limit`。
|
|
263
364
|
// 消费点写在 `outcome.kind === 'failed'` 之内:决断成功的那一轮压根不该动这个记号。
|
|
365
|
+
//
|
|
366
|
+
// 🔴 #357(现网 P0,[5522]/[5525]):上面那条一次性只关掉了**身份腿**的空转;它的孪生腿
|
|
367
|
+
// (code/文案判据)当时没有任何限次 —— 「run 仍 parked 而取件读到空表」的**坐标失配**态下,
|
|
368
|
+
// 每一轮重放都命中 code 臂,照样烧满 24 个 hop、16 分钟长等、终帧还是那句与真因无关的预算话术
|
|
369
|
+
// ([same-shape-residue-constitution]:同一病形的第二处存量)。下面两件按 `MAX_ALREADY_RESOLVED_REATTACHES`
|
|
370
|
+
// 头注的分工落地:主修 = 坐标重探(拿当前拍的坐标真把新卡呈出去),兜底 = 同因连续限次。
|
|
264
371
|
const alreadyDecidedById = outcome.kind === 'failed' &&
|
|
265
372
|
candidateGatedCallId !== undefined &&
|
|
266
373
|
led.takeDecided(candidateGatedCallId);
|
|
267
374
|
if (outcome.kind === 'failed' && (isAlreadyResolvedFailure(outcome) || alreadyDecidedById)) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
375
|
+
const firstReason = outcome.reason;
|
|
376
|
+
// 键按语义分(见 `alreadyResolvedStreakKey` 头注);命中的那个文案词只进诊断串。
|
|
377
|
+
const streakKey = alreadyResolvedStreakKey(!isAlreadyResolvedFailure(outcome) && alreadyDecidedById);
|
|
378
|
+
const reasonToken = alreadyResolvedReasonToken(outcome.reason);
|
|
379
|
+
// ── 主修:坐标重探 ────────────────────────────────────────────────────────────────────
|
|
380
|
+
// 🔴 重探 = **按当前拍再读一次队列**(同一条呈卡腿、**同样的入参**)。有可决行 ⇒ 这一轮就把
|
|
381
|
+
// 新卡真呈给用户(死锁在这里破);无行 ⇒ 它照旧回同一个「已解决」,落下面的限次闸。
|
|
382
|
+
// 坐标轮换在这里是**自动**发生的:取件腿的身份腿在队列里找不到那张 park 的陈旧 callId 时
|
|
383
|
+
// 本就回落 taskId + 族谓词两腿,于是选中的正是当前拍的新行(server [5525] 的新 token 新行)。
|
|
384
|
+
// 🔴 **刻意不把身份入参抹成 `undefined`**(异源门 F10-j 当场证伪的写法):`parkGatedCallId` 在
|
|
385
|
+
// `findPendingForTask` 里同时是「优先命中的身份」**和**「身份与本腿族属冲突时把候选池收紧」
|
|
386
|
+
// 的那把闸。抹掉它就等于把 F10-j 钉住的那条绕道兜底重新打开 —— 身份指着一行反族行、队列里
|
|
387
|
+
// 又没有别的行时,「任意行」兜底会把那行反族行捞回来呈卡 + decide(卡面全错、决断打在另一个
|
|
388
|
+
// checkpoint 上)。重探要的是**读数新鲜**,不是**判据放宽**。
|
|
389
|
+
const rescan = isFetchStepNoPending(outcome) && hops <= MAX_GATE_HOPS
|
|
390
|
+
? await surfaceParkGate(park, ctx, park.gatedCallId)
|
|
391
|
+
: undefined;
|
|
392
|
+
// 🔴 只有**真进展**才采信重探的结果:决断成功 / 用户 Esc / 瞬断耗尽(那一形有自己的重呈臂)。
|
|
393
|
+
// 重探自己失败(网络类、或队列确实没有可决行)⇒ **不采信**,走限次闸兜底 —— 采信的话,
|
|
394
|
+
// 一次瞬时读面故障就会把 #110 那条「重放的 park 不是失败」救回腿改判成 fail-soft(会话废掉
|
|
395
|
+
// 那条路径),代价严重不对称。
|
|
396
|
+
if (rescan !== undefined &&
|
|
397
|
+
(rescan.kind === 'decided' ||
|
|
398
|
+
rescan.kind === 'aborted' ||
|
|
399
|
+
(rescan.kind === 'failed' && rescan.retryExhausted === true))) {
|
|
400
|
+
led.resetAlreadyResolvedGate();
|
|
401
|
+
hostLog('debug', `liveHitlAskWire: gate reported already-resolved (${firstReason}) but a fresh approvals re-read for run ` +
|
|
402
|
+
`${taskId} surfaced a decidable row under the current coordinates — re-presented it (rescan: ${rescan.kind}) ` +
|
|
403
|
+
`instead of re-attaching on the stale park identity`);
|
|
404
|
+
outcome = rescan;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const streak = led.noteAlreadyResolvedGate(streakKey);
|
|
408
|
+
if (streak <= MAX_ALREADY_RESOLVED_REATTACHES) {
|
|
409
|
+
const seq = led.lastSeq();
|
|
410
|
+
hostLog('debug', `liveHitlAskWire: gate already resolved (${firstReason}) — replayed park, re-attaching runs.events(${taskId})${seq ? ` from seq ${seq}` : ''} instead of failing the turn` +
|
|
411
|
+
(alreadyDecidedById ? ` [decidedGates id match: ${candidateGatedCallId}]` : '') +
|
|
412
|
+
(led.decidedCount() > 0 ? ` [decided so far: ${led.decidedCount()}]` : ''));
|
|
413
|
+
return { kind: 'reattach' };
|
|
414
|
+
}
|
|
415
|
+
// ── 兜底:同因连续第 2 次 ⇒ 不再空转,诚实收场 ──────────────────────────────────────
|
|
416
|
+
// 件B(可见告知):真因走**终帧的 errorMessage**(下面 fail-soft 汇流合成的那一条),不是只落
|
|
417
|
+
// debug —— 修前用户在 24 轮里什么都看不到,最后拿到的是一句关于 hop 预算的话。
|
|
418
|
+
// 🔴 如实记边界:`park.pendingDone` 在场(sync leg 的 `done{suspended}`)时,fail-soft 汇流
|
|
419
|
+
// 按既有语义原样吐那一帧、不合成 failed —— 那一格的告知面仍只有本行日志。durable 腿
|
|
420
|
+
// (真实病形所在:`suspended` 事件无 pendingDone)恒走合成帧,真因必然上屏。
|
|
421
|
+
const stalled = `the gate reported already-resolved ${streak} times in a row while the run stayed parked, and a fresh ` +
|
|
422
|
+
`approvals re-read surfaced no decidable row for it — run/approvals coordinate mismatch ` +
|
|
423
|
+
`(last gate reason: ${firstReason})`;
|
|
424
|
+
hostLog('warn', `liveHitlAskWire: refusing to re-attach runs.events(${taskId}) again — ${stalled} ` +
|
|
425
|
+
`[criterion: ${streakKey}${reasonToken !== undefined ? ` via reason token "${reasonToken}"` : ''}]`);
|
|
426
|
+
outcome = {
|
|
427
|
+
kind: 'failed',
|
|
428
|
+
...(outcome.gatedCallId !== undefined ? { gatedCallId: outcome.gatedCallId } : {}),
|
|
429
|
+
reason: stalled,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
273
432
|
}
|
|
274
433
|
// Inkglow-1085 P0a —— **重呈臂**:decide 出站在瞬断类失败上重试一次仍未送达(两发都没拿到引擎的
|
|
275
434
|
// 语义答复)。此刻 run 仍 parked、pending 行仍可决 —— 合成 `hitl_unanswered` 把 turn 判死是三条
|
|
@@ -325,6 +484,9 @@ export async function resolvePark(park, ctx) {
|
|
|
325
484
|
// `park.gatedCallId`(server ≥7.41.0 的 wire 身份)在场就点名摘,缺席则只在同族唯一时摘。
|
|
326
485
|
// 这一行**刻意排在 `if (outcome.gatedCallId)` 之外**:身份在 wire 上时,摘不摘与 pending
|
|
327
486
|
// 行给没给出 callId 无关(park 有主角,decide 也成功了)。
|
|
487
|
+
// #357:决断成功 = 判据链上的**真进展**,「已解决」的同因连续计数归零(否则一个 turn 里两只门
|
|
488
|
+
// 各带一次正当重放,第二只的重放会被第一只的计数顶成「第 2 次」而被闸掉)。
|
|
489
|
+
led.resetAlreadyResolvedGate();
|
|
328
490
|
led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
|
|
329
491
|
if (outcome.gatedCallId) {
|
|
330
492
|
// 🔴 #324:**刻意不把 pending 行的 callId 当成连坐判别的主角身份**。durable `checkpointGate`
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
* 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
|
|
78
78
|
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
79
79
|
*/
|
|
80
|
-
import { type HitlClientLike } from './hitlBridge.js';
|
|
80
|
+
import { type GateCurrentPending, type HitlClientLike } from './hitlBridge.js';
|
|
81
81
|
import type { RuleSuggestion, ToolApprovalRespondAck } from '@sema-agent/sdk';
|
|
82
82
|
/** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
|
|
83
83
|
* [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
|
|
@@ -108,12 +108,26 @@ export type FsApprovalOutcome = {
|
|
|
108
108
|
/** `retryExhausted`(Inkglow-1085 P0a,0.37.0):decide 出站在**瞬断类**失败上重试一次仍未送达
|
|
109
109
|
* (两发都没拿到引擎的语义答复)—— 在场即真。消费方(parkResolver)据此走**重呈臂**(re-attach
|
|
110
110
|
* 重放 park ⇒ 同一张卡重新交给用户),不合成 `hitl_unanswered` 判死 turn。缺席 = 语义类失败
|
|
111
|
-
* (引擎答了且拒了)或卡面失败,既有 fail-soft 语义逐字节不变。
|
|
111
|
+
* (引擎答了且拒了)或卡面失败,既有 fail-soft 语义逐字节不变。
|
|
112
|
+
* `code`(#357,0.47.0):**取件当拍队列里没有本 run 的可决行** —— `findPendingForTask` 的
|
|
113
|
+
* 结构化判别位原样透传(此前本类型没有这个位,消费点只能拿 `reason` 的文案子串猜,而
|
|
114
|
+
* `parkResolver.isAlreadyResolvedFailure` 的头注把「fs 腿没有 code」记成了已知缺口)。
|
|
115
|
+
* 🔴 只透传取件腿那一个码:decide 失败(409/404 语义答复)不落这个位 —— 那两件事对消费点
|
|
116
|
+
* 是不同处置(前者「这一拍没有行」可以重探,后者是引擎的语义答复)。 */
|
|
117
|
+
/** `currentPending`(#363,0.47.0):**decide 失败**那一类才可能带 —— server S-02(≥7.52)在
|
|
118
|
+
* 409 `approval_stale` 拒体上铸的 additive **指路键**,说的是「你手上那条已经不是当前的了,
|
|
119
|
+
* **当前**这条长这样」。壳据此**一跳重定位**再 decide,免整队重拉。
|
|
120
|
+
* 🔴 缺席是常态,而且缺席**什么都不证明**:老 server / 非工具门 / 行读失败 / 本次失败根本
|
|
121
|
+
* 不是 stale 臂,四种情形在 wire 上同形(server 自己的 F 类留痕账逐字如此)。
|
|
122
|
+
* 🔴 它**不参与任何裁决** —— 见 `GateCurrentPending` 顶注;消费方拿它当指路,不当放行凭据。
|
|
123
|
+
* 🔴 **取件腿的 `no_pending` 恒不带这一位**:那一腿连 decide 都没发,谈不上「你决的那条过期了」。 */
|
|
112
124
|
| {
|
|
113
125
|
kind: 'failed';
|
|
114
126
|
gatedCallId?: string | undefined;
|
|
115
127
|
reason: string;
|
|
128
|
+
code?: 'no_pending' | undefined;
|
|
116
129
|
retryExhausted?: true;
|
|
130
|
+
currentPending?: GateCurrentPending | undefined;
|
|
117
131
|
};
|
|
118
132
|
/** 本桥消费的 wire 面(liveHitlAskWire 的 AskGateWireDeps 同形切片,mock 可注入)。 */
|
|
119
133
|
export interface FsApprovalWireDeps {
|
|
@@ -401,6 +415,26 @@ export interface ApprovalCardRequest {
|
|
|
401
415
|
* `renderUntrustedCommandText`(core 导出的展示基线)。本包只保证这一位到得了卡口。
|
|
402
416
|
*/
|
|
403
417
|
inputHasBidi?: true;
|
|
418
|
+
/**
|
|
419
|
+
* **S-30①(0.48.0;server ≥7.53 / core 5.60.0 #438;sdk 7.4.0 起在 `PendingCheckpoint` 上声明)**
|
|
420
|
+
* —— 原样来自 **durable park 行** `PendingCheckpoint.hasBidiControls`:这条 park 行的**执行载荷**里
|
|
421
|
+
* 含至少一个 DIRECTIONAL 格式控制符(Trojan Source:人眼读到的顺序 ≠ 真正执行的字节顺序)。
|
|
422
|
+
*
|
|
423
|
+
* 🔴 **与 {@link inputHasBidi} 刻意分键不合流**(上游把名字取得不同,正是为了不让人合并):
|
|
424
|
+
* · {@link inputHasBidi} = **同步腿**(`tool_approval` 活卡帧),server 对**帧自身序列化后的 args**
|
|
425
|
+
* 现算(E-14);
|
|
426
|
+
* · 本键 = **durable 腿**,值**完全由 core 供**(park mint 时 `PendingAction.hasBidiControls`,
|
|
427
|
+
* 反范式成 durable 行的 `has_bidi_controls` 列;server `listPending` 读列 `=== 1` 才铸,**不重算**)。
|
|
428
|
+
* 两者是**同语义、不同源、不同算点**的两个量 ⇒ 同一只 ask 的两条腿在场性**可以不一致**,合成一位
|
|
429
|
+
* 就是拿一个量冒充另一个([same-name-different-meaning-crosses-layers])。端要渲一个徽标的话,
|
|
430
|
+
* 读**两位的并**是允许的(那是端的呈现决定),但两位在本层必须各自到货。
|
|
431
|
+
* 🔴 **缺席绝不折成 `false`**(类型 `true`,与 {@link governanceForced} / {@link inputHasBidi} 同族):
|
|
432
|
+
* 缺席 = **没检出**(干净 / core 有界扫描没够着 / 列诞生前 park 的老行),消费端**禁**读成
|
|
433
|
+
* 「已确认干净」—— 那是对用户下一个证不出的断言。
|
|
434
|
+
* 🔴 **披露位,不是清洗位;本包字节零改**:清洗会改掉即将被执行的那串字节(卡上显示的与真跑的不是
|
|
435
|
+
* 同一个东西),比不披露更坏。显形(转义/高亮/加标记)归端。
|
|
436
|
+
*/
|
|
437
|
+
hasBidiControls?: true;
|
|
404
438
|
/**
|
|
405
439
|
* 被越级的持久 allow 规则**原文**(#144,原样来自 {@link ToolApprovalFrame.persistedRuleShadowed}
|
|
406
440
|
* 的合形值)——壳据此渲「你的规则仍在,只是这次调用被要求逐次确认」;缺席 = 卡形与 0.27.0
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
* 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
|
|
78
78
|
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
79
79
|
*/
|
|
80
|
-
import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, } from './hitlBridge.js';
|
|
80
|
+
import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, readDecideCurrentPending, } from './hitlBridge.js';
|
|
81
81
|
import { hostLog } from '../host.js';
|
|
82
82
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
83
83
|
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
@@ -251,8 +251,10 @@ parkGatedCallId) {
|
|
|
251
251
|
// toolName + fs/shell 名字腿),而上面那个 `matches` 只有名字腿那半 —— 拿它当身份闸会把合法的
|
|
252
252
|
// kind-only 行判出局(五审 finding①)。gateKind = 行上的扁平投影,与 gate.kind 同一权威。
|
|
253
253
|
(row) => isToolApprovalGate({ kind: row.gateKind, toolName: row.toolName }));
|
|
254
|
+
// #357:取件腿的结构化判别位原样透传(见 `FsApprovalOutcome.code` 头注)——「这一拍队列里没有
|
|
255
|
+
// 本 run 的可决行」与「approvals.list 自身炸了」自此对消费点结构可分,不再靠文案子串猜。
|
|
254
256
|
if (!found.ok)
|
|
255
|
-
return { kind: 'failed', reason: found.reason };
|
|
257
|
+
return { kind: 'failed', reason: found.reason, ...(found.code !== undefined ? { code: found.code } : {}) };
|
|
256
258
|
const { pending, gatedCallId } = found;
|
|
257
259
|
const toolName = typeof pending.toolName === 'string' ? pending.toolName : '';
|
|
258
260
|
// 工具入参:优先流上 tool_start.args(完整、零额外语义);缺则 pending.input(service 已 redact+限长
|
|
@@ -293,6 +295,13 @@ parkGatedCallId) {
|
|
|
293
295
|
callKey,
|
|
294
296
|
...(signal ? { signal } : {}),
|
|
295
297
|
...(pending.governanceForced === true ? { governanceForced: true } : {}),
|
|
298
|
+
// S-30①(0.48.0):durable 行的 bidi 披露位随卡透传。**同一个病形的第 N 例** —— 这处「行 → 卡」
|
|
299
|
+
// 重铸是闭形的,server ≥7.53 起行上就有的这一位在这里整键丢失([C170] 那次 ruleSuggestions /
|
|
300
|
+
// governanceForced 逐字同一个坐标)。本批由 durable-card 门 ⑦ 段的**富行键集普查**当天抓出
|
|
301
|
+
// (红文逐字:「无账:hasBidiControls」),不是人眼发现的。
|
|
302
|
+
// 🔴 条件 stamp **只认严格 true**,与 governanceForced / inputHasBidi 同族:上游契约是
|
|
303
|
+
// 「`true` 或缺席,恒不写 false/null」,这里认宽了就会把一个 falsy 值渲成披露。
|
|
304
|
+
...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
|
|
296
305
|
...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
|
|
297
306
|
...(isWireRecordCarrier(durableProbeCause) ? { probeCause: durableProbeCause } : {}),
|
|
298
307
|
// #348(0.44.0)durable 腿的对偶:行上**本来就存着**这个值(server `parked-decide.ts`;SDK
|
|
@@ -337,8 +346,20 @@ parkGatedCallId) {
|
|
|
337
346
|
// Inkglow-1085 P0a:瞬断耗尽同样**不许**回退纯 approve —— 引擎此刻根本够不着,再补一发
|
|
338
347
|
// 纯 approve 只是再烧一轮超时,还把「传输断了」错标成「老 server 不识别 remember」。
|
|
339
348
|
// 上抛给外层 catch ⇒ retryExhausted 判别位 ⇒ parkResolver 走重呈臂。
|
|
349
|
+
// 🔴 **第三条再抛(#363,0.47.0;异源复审 [high] 采纳,真病)**:拒体带 `currentPending`
|
|
350
|
+
// ⇒ 引擎**结构化地告诉我们**「你决的那一行已经不是当前的了,当前是这一条」——
|
|
351
|
+
// 那和 `binding_mismatch` 是同一件事的另一种说法(人看见的那一行在他决断期间被换掉了),
|
|
352
|
+
// 只是走的是 stale 那条码。修前它落进这条 catch-all,被当成「老 server 不识别 remember」
|
|
353
|
+
// **并立刻用人对旧卡给的答案再发一次纯 approve** —— 正是本臂上方那条铁律禁的动作;
|
|
354
|
+
// 而且顺带把这枚指路键吞掉(第二发若也失败,外层拍平拿到的是**第二个**错误,首发的
|
|
355
|
+
// `currentPending` 从此不存在)。⇒ 上抛,由外层拍平臂原样带上指路键,人重新决断。
|
|
356
|
+
// ⚠️ 边界如实说:本条只收「引擎给了指路键」这一格;「回退臂是不是应该收窄成**只**兜
|
|
357
|
+
// 400 未知键」是**本批之前就在的**分类问题(见 §7b 登记),不在本批射程内 ——
|
|
358
|
+
// 在一个 additive 批里悄悄改掉老引擎兼容腿的宽度,是另一种越界。
|
|
340
359
|
if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
|
|
341
360
|
throw e;
|
|
361
|
+
if (readDecideCurrentPending(e) !== undefined)
|
|
362
|
+
throw e;
|
|
342
363
|
hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
|
|
343
364
|
}
|
|
344
365
|
}
|
|
@@ -346,12 +367,15 @@ parkGatedCallId) {
|
|
|
346
367
|
return { kind: 'decided', gatedCallId };
|
|
347
368
|
}
|
|
348
369
|
catch (e) {
|
|
370
|
+
// #363:S-02 指路键原样透传(读不出 ⇒ 键不铸,形状逐字节不变)。见 FsApprovalOutcome 同名位。
|
|
371
|
+
const currentPending = readDecideCurrentPending(e);
|
|
349
372
|
return {
|
|
350
373
|
kind: 'failed',
|
|
351
374
|
gatedCallId,
|
|
352
375
|
reason: `decide(approve) failed: ${String(e)}`,
|
|
353
376
|
// Inkglow-1085 P0a:瞬断耗尽的判别位(在场即真)—— 语义失败缺席,reason 字节不变。
|
|
354
377
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
378
|
+
...(currentPending !== undefined ? { currentPending } : {}),
|
|
355
379
|
};
|
|
356
380
|
}
|
|
357
381
|
case 'deny':
|
|
@@ -364,11 +388,14 @@ parkGatedCallId) {
|
|
|
364
388
|
return { kind: 'decided', gatedCallId, denied: true };
|
|
365
389
|
}
|
|
366
390
|
catch (e) {
|
|
391
|
+
// #363:与 allow 臂**同形同源**(同形存量清剿:两条 decide 失败腿一次改齐,不许只修当格)。
|
|
392
|
+
const currentPending = readDecideCurrentPending(e);
|
|
367
393
|
return {
|
|
368
394
|
kind: 'failed',
|
|
369
395
|
gatedCallId,
|
|
370
396
|
reason: `decide(deny) failed: ${String(e)}`,
|
|
371
397
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
398
|
+
...(currentPending !== undefined ? { currentPending } : {}),
|
|
372
399
|
};
|
|
373
400
|
}
|
|
374
401
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -138,6 +138,7 @@ export * from './notifications.js';
|
|
|
138
138
|
export * from './steering.js';
|
|
139
139
|
export * from './diagnostics.js';
|
|
140
140
|
export * from './retryStatus.js';
|
|
141
|
+
export * from './sessionMemoryStatus.js';
|
|
141
142
|
export * from './adapt.js';
|
|
142
143
|
export * from './subagentContentStore.js';
|
|
143
144
|
export * from './engineAgentPanelStore.js';
|
|
@@ -237,6 +238,7 @@ export * from './hitl/resumeRunningCard.js';
|
|
|
237
238
|
export * from './hitl/persistedRulesWire.js';
|
|
238
239
|
export * from './hitl/localAllowRule.js';
|
|
239
240
|
export * from './hitl/approvalsFeed.js';
|
|
241
|
+
export * from './interactiveHalt.js';
|
|
240
242
|
export * from './compensations.js';
|
|
241
243
|
export * from './request/printNotification.js';
|
|
242
244
|
export * from './seatContract.js';
|