@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.
- package/CHANGELOG.md +313 -0
- package/README.md +2 -2
- package/dist/adapt/arms.js +43 -0
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +2 -0
- package/dist/adapter/activeRunSelfHeal.d.ts +35 -2
- package/dist/adapter/activeRunSelfHeal.js +157 -4
- package/dist/adapter/downstream/eventToSdkMessage.js +99 -0
- package/dist/adapter/runStream.d.ts +14 -1
- package/dist/adapter/runStream.js +4 -0
- package/dist/engineCapsCache.d.ts +81 -3
- package/dist/engineCapsCache.js +183 -15
- package/dist/engineErrorCodes.d.ts +20 -0
- package/dist/engineErrorCodes.js +44 -0
- package/dist/fleet/fleetProjection.d.ts +43 -1
- package/dist/fleet/fleetProjection.js +53 -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 +215 -12
- package/dist/hitl/parkResolver.d.ts +1 -0
- package/dist/hitl/parkResolver.js +22 -2
- package/dist/hitl/toolApprovalWire.d.ts +41 -6
- package/dist/hitl/toolApprovalWire.js +40 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/model/modelSupplyRules.d.ts +102 -0
- package/dist/model/modelSupplyRules.js +149 -0
- package/dist/model/providerPresets.js +68 -12
- package/dist/request/taskRequest.js +11 -11
- package/dist/retryStatus.d.ts +38 -3
- package/dist/retryStatus.js +15 -5
- package/dist/seam.d.ts +48 -1
- package/dist/seam.js +7 -0
- package/dist/subagent/engineSubagentResume.d.ts +18 -0
- package/dist/subagent/engineSubagentResume.js +7 -0
- package/dist/toolResult.d.ts +8 -0
- package/dist/toolResult.js +15 -0
- package/docs/INTEGRATION-CLIENTS.md +69 -20
- package/package.json +4 -4
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
|
|
2
2
|
import { eventSeq } from '../adapter/types.js';
|
|
3
3
|
import { hostLog } from '../host.js';
|
|
4
|
+
import { abortableSleep } from '../abortableSleep.js';
|
|
4
5
|
// ── deny/plan-review 归因的 wire 窄化(0.28.0 发版扫描 F1/F2/F3 收编;单源,三条腿共用)──────
|
|
5
6
|
//
|
|
6
7
|
// server 对 `/decide` 与 plan-review 两条腿的 `reason` 同限 `MAX_APPROVAL_REASON_CHARS`(4096 字符,
|
|
@@ -52,6 +53,166 @@ export class HitlSafetyError extends Error {
|
|
|
52
53
|
this.name = 'HitlSafetyError';
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
// ── decide 出站的瞬断分诊与有界重试(Inkglow-1085 P0a,2026-08-19;[4664] server 定谳后对表)────
|
|
57
|
+
//
|
|
58
|
+
// 病形(案卷 INKGLOW-1085 车1 全链):`approvals.decide` 是**非 submit POST** —— SDK transport 对它
|
|
59
|
+
// `maxAttempts = 1`(「a decide must not double-act」),且每发带显式 `AbortSignal.timeout(timeoutMs)`
|
|
60
|
+
// (缺省 60s;per-call opts 只有 `signal`,调用方 signal 与之**合流取先**,只能收短、不能放长 ——
|
|
61
|
+
// timeoutMs 是 client 构造级旋钮,本包对已构造的注入 client 不可配)。于是一次网络瞬断/超时 =
|
|
62
|
+
// decide 单发即死 → 上层拼 `failed` → parkResolver 合成 `hitl_unanswered` 终帧 → ask 死局。
|
|
63
|
+
//
|
|
64
|
+
// 🔴 **口径换代(#318 件②,2026-08-21):超时从「常态」变「真异常」** —— [4664] 的长调用口径作废。
|
|
65
|
+
// server #316([4687] 点名 BREAKING,随 7.37.0 发车)把任务级 decide 的 200 体从**终局形**改成
|
|
66
|
+
// **受理回执** `{taskId, sessionId, status:"resuming", bindingEnforced:true}`:受理点设在 core
|
|
67
|
+
// `resumeStream` 解析之后,一切会变成拒绝的判定(lease 429 / markResuming CAS 409 / core pre-CAS
|
|
68
|
+
// 守卫 / 绑定不符 409 / 卡不在 404)**仍同步发生**,受理后只剩模型段异步跑。实测受理即回 ≈23ms。
|
|
69
|
+
// ⇒ 对 ≥7.37 的部署,一次 60s 超时**不再是**「server 还在跑 resume」的常态,而是**真异常**
|
|
70
|
+
// (网络路径断在半途 / server 病态卡死 / 见下两条残余形)。口径随之收窄:
|
|
71
|
+
// · **超时类**(TimeoutError = SDK per-attempt 帽掐断)⇒ 仍带退避重试(重试环结构不动),但总窗
|
|
72
|
+
// 从 10 分钟收到 {@link DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS}(十秒级,见该常量的取值推导);
|
|
73
|
+
// · **网络断类**(ECONNREFUSED / fetch failed 等:连语义答复都没拿到,引擎多半真死)⇒ 重试
|
|
74
|
+
// **恰一次**,让真死尽快显形;
|
|
75
|
+
// · 语义答复类(带 HTTP status 的 4xx/5xx —— 引擎收到并回答了)⇒ 零重试,原样上抛;
|
|
76
|
+
// · `HitlSafetyError`(binding mismatch 等安全信号)⇒ 恒零重试(§9.1 铁律,原有行为不动);
|
|
77
|
+
// · 调用方已中止(用户 Esc)⇒ 零重试(AbortError 是人按的,不是瞬断)。
|
|
78
|
+
//
|
|
79
|
+
// 🔴 为什么重试不违反「decide 绝不 double-act」:①[4664] 原话 —— **重复 decide 不双跑,server CAS
|
|
80
|
+
// 保证**(重试幂等安全,放心重试);②D-1 绑定回显(boundCallId+boundInputHash 逐字回显,server
|
|
81
|
+
// fail-closed on mismatch)—— 第一发其实送达时,重发只能撞 4xx(approval not found / conflict),
|
|
82
|
+
// 绝不可能批掉**另一件**事;那个 4xx 原样上抛,parkResolver 的「已解决重放救回」判据照认。
|
|
83
|
+
// 重试耗尽(超时类窗尽 / 网络断类第二发仍断)⇒ 抛 {@link DecideTransportRetryExhaustedError}
|
|
84
|
+
// (typed 判别位)—— 消费方(toolApprovalWire / parkResolver)据此走**重呈臂**而不是把 turn 判死。
|
|
85
|
+
// 引擎真死时失败也会尽快显形:重呈臂的下一步(approvals.list / runs.events)对死引擎当场失败,
|
|
86
|
+
// 走既有诚实红。
|
|
87
|
+
// 🔴 **两条残余的同步形如实登记**(收窄不是「长调用消失了」,[4687] 逐字):终局形仍存在于
|
|
88
|
+
// ① **pre-7.37 的 server**(所有 decide 都是终局形 = 真长调用);② **≥7.37 但没有 durable run 行
|
|
89
|
+
// 可跟的部署**(那里提前受理 = 把结果扔掉,所以腿如实保持同步)。本包是三端共用件,面向的是
|
|
90
|
+
// 任意部署 —— 所以重试环**保留**、总窗**不设 0**。这两形上窗尽的代价是可接受的:耗尽走的是
|
|
91
|
+
// {@link DecideTransportRetryExhaustedError} → **重呈臂**(re-attach ⇒ durable 流重放 park ⇒
|
|
92
|
+
// 同一张卡重交用户),而不是把 turn 判死;若第一发其实已送达,重呈的下一步会撞
|
|
93
|
+
// `isAlreadyResolvedGateReason` 的已解决判据被救回。
|
|
94
|
+
// 🔴 请托半场(候黑板):SDK decide 若开 per-call timeoutMs(或对 HITL 面单列长缺省),本层的
|
|
95
|
+
// 超时类重试环可整段收敛成一发长等待。
|
|
96
|
+
/** 瞬断重试的起始退避(×2 递增,封顶 {@link DECIDE_RETRY_BACKOFF_MAX_MS};别把 decide 打成连发)。 */
|
|
97
|
+
const DECIDE_TRANSPORT_RETRY_BACKOFF_MS = 750;
|
|
98
|
+
const DECIDE_RETRY_BACKOFF_MAX_MS = 5_000;
|
|
99
|
+
/**
|
|
100
|
+
* SDK 的 **per-attempt 超时帽**(`AbortSignal.timeout(timeoutMs)`)的**缺省值**。
|
|
101
|
+
*
|
|
102
|
+
* 🔴 **它是「观察到的缺省」,不是本包能保证的量**(codex 对抗复审 [medium] 采纳,2026-08-21)。
|
|
103
|
+
* per-call opts 只有 `signal`(与之合流取先,只能收短不能放长),但 `timeoutMs` 是 **client
|
|
104
|
+
* 构造级**旋钮,而本桥吃的是**宿主注入的** client(`HitlClientLike` 根本不暴露它)⇒ 一个 web/桌面
|
|
105
|
+
* 宿主完全可以用 30s 或 120s 的 client 构造本桥。所以任何「总窗 ÷ 帽 = 发数」的推导都只在缺省值
|
|
106
|
+
* 上成立,**不能当成本共用件的性质**。
|
|
107
|
+
* ⇒ 本常量只用来推导下面那个**墙钟上界**;「至少重试一次」这条**性质**改由
|
|
108
|
+
* {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 用**发数**保证,与宿主的 timeoutMs 无关。
|
|
109
|
+
*/
|
|
110
|
+
const DECIDE_ATTEMPT_TIMEOUT_CAP_MS = 60_000;
|
|
111
|
+
/**
|
|
112
|
+
* 超时类**最少发数**(首发 + 至少一次重试)—— 与墙钟窗**两个独立的界**,不是第二层节奏。
|
|
113
|
+
*
|
|
114
|
+
* 🔴 为什么必须有它([4687] 登记的两条残余同步形是承重理由):终局形仍存在于 pre-7.37 的 server
|
|
115
|
+
* 与「≥7.37 但没有 durable run 行可跟」的部署 —— 那两形上一次超时**极可能是真的还在跑**,
|
|
116
|
+
* 至少给一次重试是这条腿唯一的补救。若只用墙钟窗判,宿主拿 120s 的 client 构造本桥时首发超时
|
|
117
|
+
* 那一刻 elapsed 已经 ≥ 窗 ⇒ **一次重试都没有**,而这件事在代码里毫无痕迹(注释还写着「恰一次重试」)。
|
|
118
|
+
* 🔴 这**不是** [4675] 说的「第二层节奏叠乘」:没有新增任何定时器/退避层,退避仍是同一条
|
|
119
|
+
* `backoff` 链;这只是同一个循环上的第二个**终止条件**(发数尽 ∧ 窗尽,两者都满足才停)。
|
|
120
|
+
*/
|
|
121
|
+
const DECIDE_TIMEOUT_MIN_ATTEMPTS = 2;
|
|
122
|
+
/**
|
|
123
|
+
* 超时类重试的总窗(#318 件② 收窄:`10 * 60_000` → 本值)。
|
|
124
|
+
*
|
|
125
|
+
* ── 取值推导(锚在 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS} 上,不是拍脑袋的整数)────────────────
|
|
126
|
+
* 窗的语义是**不再起新发**(见下),而每一发超时类失败**本身**就要吃满一个 per-attempt 帽 ⇒
|
|
127
|
+
* 实际发数由「总窗 ÷ 帽」决定,且量化得很粗:
|
|
128
|
+
* · 窗 ≤ 1 帽 ⇒ 窗判本身当场耗尽;
|
|
129
|
+
* · 1 帽 < 窗 ≤ 2 帽 ⇒ 窗判允许恰一次重试(共 2 发);
|
|
130
|
+
* · > 2 帽 ⇒ 3 发起步,一路回到分钟级。
|
|
131
|
+
* 受理形下超时是**真异常**(不是「还在跑」),所以取**恰一次重试**那一档:一次重试足够吃掉单次
|
|
132
|
+
* 网络抖动,再多就是对着一个病态 server 空等。取 90s = 1.5 帽,**刻意落在区间中部**而不是边界
|
|
133
|
+
* (120s 恰等于 2 帽 + 退避,会让发数悬在退避时序的一根头发上)。
|
|
134
|
+
* ⇒ 在缺省帽上可预算的最坏墙钟 ≈ 60s(首发)+ 0.75s(退避)+ 60s(重发)≈ 121s,而不是旧口径的 10 分钟。
|
|
135
|
+
*
|
|
136
|
+
* 🔴 **本窗只是墙钟上界,不承诺发数**(codex [medium] 采纳):宿主可以用非缺省 `timeoutMs` 构造
|
|
137
|
+
* client(见 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS}),那时「窗 ÷ 帽」得出的发数与这里写的不同。
|
|
138
|
+
* 与宿主无关的那条性质(**至少重试一次**)由 {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 单独保证 ——
|
|
139
|
+
* 两个界合取:**发数达标 ∧ 窗尽** 才停。所以在 120s client 上是「2 发、~240s」,在 30s client 上
|
|
140
|
+
* 是「3 发、~92s」,在缺省 60s 上是「2 发、~121s」—— 三者都有界,且都拿得到那一次重试。
|
|
141
|
+
*
|
|
142
|
+
* 🔴 窗的语义是**不再起新发**,刻意不掐在飞那一发(codex 复审议题,驳回后成文):给一发可能已被
|
|
143
|
+
* server 受理的 decide 塞截止 signal 换不来任何安全 —— server 侧照跑,客户端只多制造一个「送达
|
|
144
|
+
* 未知」。
|
|
145
|
+
* 🔴 **写成「帽 × 系数」而不是裸 90_000**:上面那段推导只有在两者绑在一起时才会随 SDK 改帽自动
|
|
146
|
+
* 跟手;写裸整数的话,SDK 哪天把帽改成 30s,注释里的「恰一次重试」当天变成假话而代码全绿。
|
|
147
|
+
*/
|
|
148
|
+
const DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE = 1.5;
|
|
149
|
+
const DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS = DECIDE_ATTEMPT_TIMEOUT_CAP_MS * DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE;
|
|
150
|
+
let decideTimeoutRetryBudgetOverrideMs;
|
|
151
|
+
/** 测试钩:把超时类重试总窗调小(传 undefined 还原缺省)。 */
|
|
152
|
+
export function __setDecideTimeoutRetryBudgetForTests(ms) {
|
|
153
|
+
decideTimeoutRetryBudgetOverrideMs = ms;
|
|
154
|
+
}
|
|
155
|
+
function decideTimeoutRetryBudgetMs() {
|
|
156
|
+
return decideTimeoutRetryBudgetOverrideMs ?? DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS;
|
|
157
|
+
}
|
|
158
|
+
/** 超时类判别(自身或 cause 链上的 TimeoutError —— SDK per-attempt `AbortSignal.timeout` 的产物;
|
|
159
|
+
* [4664] 口径下它多半意味着 server 仍在同步跑 resume,不是引擎死了)。 */
|
|
160
|
+
function isDecideAttemptTimeout(e, depth = 0) {
|
|
161
|
+
if (depth > 3 || e === null || typeof e !== 'object')
|
|
162
|
+
return false;
|
|
163
|
+
const o = e;
|
|
164
|
+
if (o.name === 'TimeoutError')
|
|
165
|
+
return true;
|
|
166
|
+
return isDecideAttemptTimeout(o.cause, depth + 1);
|
|
167
|
+
}
|
|
168
|
+
/** 瞬断类网络错误码(自身或 cause 链上;undici 的 fetch failed 把真因挂在 cause)。 */
|
|
169
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
170
|
+
'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN',
|
|
171
|
+
'ENETUNREACH', 'EHOSTUNREACH', 'ECONNABORTED',
|
|
172
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
|
|
173
|
+
]);
|
|
174
|
+
/**
|
|
175
|
+
* decide 出站失败的瞬断判别(module 私有 —— 唯一消费点是 {@link HitlBridge} 的 decide choke point)。
|
|
176
|
+
* 🔴 方向:判不出一律**非瞬断**(fail 到「不重试」侧 —— 语义拒绝被误判成瞬断才是真事故:那会把
|
|
177
|
+
* 一次已被拒绝的决断再发一遍)。带数字 `status` 的错误 = 引擎**答了**,无论 4xx/5xx 都不是瞬断。
|
|
178
|
+
*/
|
|
179
|
+
function isTransientDecideTransportFailure(e, depth = 0) {
|
|
180
|
+
if (depth > 3 || e === null || typeof e !== 'object')
|
|
181
|
+
return false;
|
|
182
|
+
if (e instanceof HitlSafetyError)
|
|
183
|
+
return false;
|
|
184
|
+
const o = e;
|
|
185
|
+
if (typeof o.status === 'number')
|
|
186
|
+
return false;
|
|
187
|
+
if (o.name === 'TimeoutError' || o.name === 'AbortError')
|
|
188
|
+
return true;
|
|
189
|
+
if (typeof o.code === 'string' && TRANSIENT_NETWORK_CODES.has(o.code))
|
|
190
|
+
return true;
|
|
191
|
+
if (typeof o.message === 'string' && o.message.toLowerCase().includes('fetch failed'))
|
|
192
|
+
return true;
|
|
193
|
+
return isTransientDecideTransportFailure(o.cause, depth + 1);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* decide 出站在瞬断类失败上重试一次**仍未送达**(两发都没拿到引擎的语义答复)。
|
|
197
|
+
*
|
|
198
|
+
* 🔴 判别位契约:消费方(`toolApprovalWire` 的 catch 臂 / `parkResolver.surfaceGateAndDecide`)
|
|
199
|
+
* 据 `instanceof` 在 outcome 上 stamp `retryExhausted: true`,parkResolver 对该位走**重呈臂**
|
|
200
|
+
* (re-attach ⇒ durable 流重放 park ⇒ 同一张卡重新交给用户),不再合成 `hitl_unanswered` 把 turn
|
|
201
|
+
* 判死 —— run 仍 parked、pending 行仍可决,判死是三条出路里唯一不可逆的那条。
|
|
202
|
+
* 🔴 message 刻意避开 `isAlreadyResolvedGateReason` 的词表(no pending checkpoint / resolved /
|
|
203
|
+
* already / not found):被那把兜底尺误判成「已解决」会走成静默 reattach,判别位就白铸了 ——
|
|
204
|
+
* pure 门(hitl-gate-honesty F6-d)有常驻负控钉着这一条。
|
|
205
|
+
*/
|
|
206
|
+
export class DecideTransportRetryExhaustedError extends Error {
|
|
207
|
+
attempts;
|
|
208
|
+
constructor(
|
|
209
|
+
/** 实际发出的次数(网络断类 = 2:首发 + 单次重试;超时类 = 总窗内发出的全部)。 */
|
|
210
|
+
attempts, lastFailureText) {
|
|
211
|
+
super(`decide did not reach the engine after ${attempts} attempts (transient transport failure): ${lastFailureText}`);
|
|
212
|
+
this.attempts = attempts;
|
|
213
|
+
this.name = 'DecideTransportRetryExhaustedError';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
55
216
|
/**
|
|
56
217
|
* The single source for "which `PendingCheckpoint` row is the human about to decide on", used by the two
|
|
57
218
|
* decision wires (`toolApprovalWire.surfaceFsApprovalAndDecide` / `askGateWire.surfaceGateAndDecide`)
|
|
@@ -323,19 +484,61 @@ export class HitlBridge {
|
|
|
323
484
|
}
|
|
324
485
|
// ── decide transport (the single choke point; maps 409 binding mismatch → safety stop) ──
|
|
325
486
|
async decideRaw(sessionId, decision, opts) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
487
|
+
// Inkglow-1085 P0a([4664] 对表后形):瞬断类失败带退避有界重试 —— 超时类按长调用总窗,
|
|
488
|
+
// 网络断类恰一次;为什么重试不违反「decide 绝不 double-act」、哪些失败绝不重试,见文件上方
|
|
489
|
+
// DECIDE_TRANSPORT_RETRY 段总注。
|
|
490
|
+
// 🔴 经函数读 aborted(waitForClaimRelease.isAborted 同注):它在 await 两侧会变,直接读两次
|
|
491
|
+
// 会被 tsc 控制流分析把第二次窄成恒假比较(TS2367)。
|
|
492
|
+
const callerAborted = () => opts?.signal?.aborted === true;
|
|
493
|
+
const startedAt = Date.now();
|
|
494
|
+
let attempts = 0;
|
|
495
|
+
let netRetriesUsed = 0;
|
|
496
|
+
let backoff = DECIDE_TRANSPORT_RETRY_BACKOFF_MS;
|
|
497
|
+
for (;;) {
|
|
498
|
+
attempts++;
|
|
499
|
+
try {
|
|
500
|
+
const r = await this.client.approvals.decide(sessionId, decision, opts);
|
|
501
|
+
// A successful decide resumes the SAME durable stream; the gate clears on the next running arm.
|
|
502
|
+
this.active = null;
|
|
503
|
+
return r;
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
if (isBindingMismatch(e)) {
|
|
507
|
+
// contract/04 §2.2 / §9.1: a binding mismatch means the pending action changed under the human.
|
|
508
|
+
// SAFETY signal — re-present to the human, NEVER auto-retry / auto-re-decide.
|
|
509
|
+
throw new HitlSafetyError('approval_binding_mismatch — the pending action changed under the human; refetch + re-present', 'binding_mismatch');
|
|
510
|
+
}
|
|
511
|
+
// 调用方已中止(用户 Esc)⇒ 这个 AbortError 是人按的,不是瞬断 —— 原错上抛,零重试。
|
|
512
|
+
if (!isTransientDecideTransportFailure(e) || callerAborted())
|
|
513
|
+
throw e;
|
|
514
|
+
if (isDecideAttemptTimeout(e)) {
|
|
515
|
+
// #318 件② 后口径:受理形(server ≥7.37)下 decide 受理即回,一次 per-attempt 帽掐断
|
|
516
|
+
// **是真异常**,不再是「server 还在跑 resume」的常态 —— 所以总窗只留恰一次重试的量
|
|
517
|
+
// (推导见 DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS)。重发仍然安全:重复 decide 由 server CAS
|
|
518
|
+
// 保证不双跑,首发其实送达时下一发只会撞 4xx(conflict/not-found ⇒ 上抛,已解决判据接手)。
|
|
519
|
+
// 🔴 **两个界合取**(codex [medium] 采纳):发数没达标就一定再发一次(与宿主的
|
|
520
|
+
// per-attempt timeoutMs 无关),达标之后才由墙钟窗决定还发不发。少了前半句,
|
|
521
|
+
// 120s client 的宿主一次重试都拿不到;少了后半句,30s client 会一路重试到分钟级。
|
|
522
|
+
if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
|
|
523
|
+
throw new DecideTransportRetryExhaustedError(attempts, String(e));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else if (netRetriesUsed >= 1) {
|
|
527
|
+
// 网络断类(连语义答复都没有)重试恰一次 —— 引擎真死时让失败尽快显形。
|
|
528
|
+
throw new DecideTransportRetryExhaustedError(attempts, String(e));
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
netRetriesUsed++;
|
|
532
|
+
}
|
|
533
|
+
hostLog('debug', `hitlBridge: decide transport failure (transient: ${String(e)}) — retrying after ${backoff}ms ` +
|
|
534
|
+
`(attempt ${attempts}; duplicate decide is CAS-safe per [4664] and the D-1 binding echo makes it ` +
|
|
535
|
+
'at-most-once-effective: a landed first shot turns the retry into a 4xx, never a second act)');
|
|
536
|
+
await abortableSleep(backoff, opts?.signal ?? new AbortController().signal);
|
|
537
|
+
backoff = Math.min(backoff * 2, DECIDE_RETRY_BACKOFF_MAX_MS);
|
|
538
|
+
// 退避期间被中止 ⇒ 不再补发(abortableSleep 对 abort 是提前 resolve,不抛)。
|
|
539
|
+
if (callerAborted())
|
|
540
|
+
throw e;
|
|
337
541
|
}
|
|
338
|
-
throw e;
|
|
339
542
|
}
|
|
340
543
|
}
|
|
341
544
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HitlBridge, HitlSafetyError, findPendingForTask } from './hitlBridge.js';
|
|
1
|
+
import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask } 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';
|
|
@@ -179,7 +179,14 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal) {
|
|
|
179
179
|
// 这里 instanceof 窄化后再过 isGateFailureCode 白名单——不认得的码(将来 hitlBridge 那边
|
|
180
180
|
// 万一多加一种)一律降级成 undefined,退回文案兜底,不让开集腐蚀这个新判别位。
|
|
181
181
|
const code = e instanceof HitlSafetyError && isGateFailureCode(e.code) ? e.code : undefined;
|
|
182
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
kind: 'failed',
|
|
184
|
+
gatedCallId,
|
|
185
|
+
reason: `decide failed: ${String(e)}`,
|
|
186
|
+
...(code !== undefined ? { code } : {}),
|
|
187
|
+
// Inkglow-1085 P0a:ask 腿与 fs 腿同形 —— 瞬断耗尽给判别位,resolvePark 走重呈臂。
|
|
188
|
+
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
189
|
+
};
|
|
183
190
|
}
|
|
184
191
|
}
|
|
185
192
|
/** 台账里那批 gated `tool_start` 当拍 args 快照 → 决断腿要的 `toolCallId → args` 表(UNTRUSTED 原样搬运)。 */
|
|
@@ -247,6 +254,19 @@ export async function resolvePark(park, ctx) {
|
|
|
247
254
|
(led.decidedCount() > 0 ? ` [decided so far: ${led.decidedCount()}]` : ''));
|
|
248
255
|
return { kind: 'reattach' };
|
|
249
256
|
}
|
|
257
|
+
// Inkglow-1085 P0a —— **重呈臂**:decide 出站在瞬断类失败上重试一次仍未送达(两发都没拿到引擎的
|
|
258
|
+
// 语义答复)。此刻 run 仍 parked、pending 行仍可决 —— 合成 `hitl_unanswered` 把 turn 判死是三条
|
|
259
|
+
// 出路里唯一不可逆的那条,只配给「引擎答了且拒了」的语义失败。这里改走 reattach:durable 流对
|
|
260
|
+
// suspended run 必然把 park 帧再送一遍(#110 已解决重放同一条机械路径),于是**同一张卡重新交给
|
|
261
|
+
// 用户**(重呈的呈现就是卡本身,归端零新 UI);hop 预算照吃(每轮都要人再答一次,不会空转)。
|
|
262
|
+
// 引擎真死时失败也尽快显形:下一轮的 approvals.list / runs.events 对死引擎当场失败,walks 既有
|
|
263
|
+
// 诚实红(reason 是 approvals.list failed,不带 retryExhausted ⇒ 不再进本臂)。
|
|
264
|
+
if (outcome.kind === 'failed' && outcome.retryExhausted === true) {
|
|
265
|
+
const seq = led.lastSeq();
|
|
266
|
+
hostLog('debug', `liveHitlAskWire: decide transport retries exhausted (${outcome.reason}) — re-presenting the gate via ` +
|
|
267
|
+
`re-attach runs.events(${taskId})${seq ? ` from seq ${seq}` : ''} instead of failing the turn (the run is still parked and the pending row is still decidable)`);
|
|
268
|
+
return { kind: 'reattach' };
|
|
269
|
+
}
|
|
250
270
|
if (outcome.kind !== 'decided') {
|
|
251
271
|
hostLog('debug', `liveHitlAskWire: gate not decided (${outcome.kind}${'reason' in outcome ? `: ${outcome.reason}` : ''}) — fail-soft to suspended terminal`);
|
|
252
272
|
const events = [...led.flushHeld()]; // 回退:毒化帧照旧渲染(= 修复前的诚实红)
|
|
@@ -104,10 +104,16 @@ export type FsApprovalOutcome = {
|
|
|
104
104
|
} | {
|
|
105
105
|
kind: 'aborted';
|
|
106
106
|
gatedCallId?: string | undefined;
|
|
107
|
-
}
|
|
107
|
+
}
|
|
108
|
+
/** `retryExhausted`(Inkglow-1085 P0a,0.37.0):decide 出站在**瞬断类**失败上重试一次仍未送达
|
|
109
|
+
* (两发都没拿到引擎的语义答复)—— 在场即真。消费方(parkResolver)据此走**重呈臂**(re-attach
|
|
110
|
+
* 重放 park ⇒ 同一张卡重新交给用户),不合成 `hitl_unanswered` 判死 turn。缺席 = 语义类失败
|
|
111
|
+
* (引擎答了且拒了)或卡面失败,既有 fail-soft 语义逐字节不变。 */
|
|
112
|
+
| {
|
|
108
113
|
kind: 'failed';
|
|
109
114
|
gatedCallId?: string | undefined;
|
|
110
115
|
reason: string;
|
|
116
|
+
retryExhausted?: true;
|
|
111
117
|
};
|
|
112
118
|
/** 本桥消费的 wire 面(liveHitlAskWire 的 AskGateWireDeps 同形切片,mock 可注入)。 */
|
|
113
119
|
export interface FsApprovalWireDeps {
|
|
@@ -347,9 +353,18 @@ export interface ToolApprovalFrame {
|
|
|
347
353
|
* lane 的 ask 出此键(宿主自身 ask 的 sourceTaskId=sessionId 已在 server 侧收口不上帧)。
|
|
348
354
|
* ⚠️ 值=子代 sessionId 非 a… handle(fence 实测 2026-07-23)——不是 fleet 台账键。 */
|
|
349
355
|
sourceTaskId?: string;
|
|
350
|
-
/**
|
|
351
|
-
*
|
|
352
|
-
|
|
356
|
+
/**
|
|
357
|
+
* core 1.378 RB-39②([1550]):Runner 填充的只读显式判别键——委派子代 gate 的 ask 恒带,
|
|
358
|
+
* 受信 internals 事实不可伪造。在场即子代(优先于 sourceTaskId 权宜式)。
|
|
359
|
+
*
|
|
360
|
+
* 🔴 **字面 `true` 不是 `boolean`**(#307 双扫 S47,2026-08-19;SDK 7.1.0/7.2.0
|
|
361
|
+
* `dist/resources/tool-approvals.d.ts` 的 `fromSubagent?: true` 逐字)——与本 interface 上
|
|
362
|
+
* `governanceForced?: true` / `requiresRealApproval?: true` 同族的**在场即真**键:
|
|
363
|
+
* wire 上只有「在场」与「缺席」两态,`false` 根本不是合法取值。此前这里写 `boolean` 是本仓
|
|
364
|
+
* 自铸的宽形,把一个不存在的第三态写进了契约。
|
|
365
|
+
* 缺席 = **没有显式判别证据**(不是「明确不是子代」),读法见 {@link isFromSubagent}。
|
|
366
|
+
*/
|
|
367
|
+
fromSubagent?: true;
|
|
353
368
|
/** core 1.378 RB-39②:展示身份(UNTRUSTED-for-display,server redact 后上帧)——徽章名一手源。 */
|
|
354
369
|
sourceAgentName?: string;
|
|
355
370
|
/**
|
|
@@ -495,8 +510,28 @@ export interface ToolApprovalDelegation {
|
|
|
495
510
|
* 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
|
|
496
511
|
*/
|
|
497
512
|
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "delegation", "outcome"];
|
|
498
|
-
/**
|
|
499
|
-
*
|
|
513
|
+
/**
|
|
514
|
+
* 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
515
|
+
* (server 1.258 [1549]①3,旧代际兼容)。
|
|
516
|
+
*
|
|
517
|
+
* 🔴 **`=== true` 是在场判别,不是布尔求值**(#307 S47 复核结论,2026-08-19)。`fromSubagent`
|
|
518
|
+
* 的 wire 形是 `?: true`(见上方声明),所以运行期只可能是 `true` 或缺席;一个**显式 `false`**
|
|
519
|
+
* 只能来自注入面或不合契约的实现,它承载的信息是「这个载体不合契约」,**不是**「引擎明确判定不是
|
|
520
|
+
* 子代」。故这里刻意**不**把 `false` 当成否定证据:它与缺席同档 —— 落回 `sourceTaskId` 在场性
|
|
521
|
+
* 那条旧代际权宜臂。
|
|
522
|
+
*
|
|
523
|
+
* 为什么这个方向是对的(而不是「显式 false ⇒ 直接 return false」):
|
|
524
|
+
* · 本判据的**唯一消费面是展示归属**(`workerBadge` 徽章 + `delegation` 出处链,见下方卡口),
|
|
525
|
+
* 不参与任何放行/收窄决策 —— 两个方向的代价不对称:多一枚徽章只是噪声,少一枚徽章是把
|
|
526
|
+
* 「这是子代发起的」这条事实对用户藏起来;
|
|
527
|
+
* · `sourceTaskId` 本身就是 server 只对后台子代 lane 才出的键(见其声明),它在场是**独立的**
|
|
528
|
+
* 子代证据。让一个不合契约的 `false` 去否决一条独立成立的证据,等于让注入面拿到一个
|
|
529
|
+
* 「隐藏子代身份」的开关;
|
|
530
|
+
* · 与本文件 `governanceForced` 的「缺席 ≠ false」同族纪律:在场即真的键上,非 `true` 一律读作
|
|
531
|
+
* 「没有这条证据」,而不是「有一条相反的证据」。
|
|
532
|
+
* 反漂移:类型侧 `?: true` 已让**编译期**的显式 false 不可能构造;本臂守的是运行期(wire/注入面)
|
|
533
|
+
* 的越界载体,pure 门有对应负控(`fromSubagent:false` + sourceTaskId 在场 ⇒ 仍判子代)。
|
|
534
|
+
*/
|
|
500
535
|
export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
|
|
501
536
|
/** 三选卡决断 → respond 端点的 wire 枚举(server parseToolApprovalDecision)。 */
|
|
502
537
|
export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
* 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
|
|
78
78
|
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
79
79
|
*/
|
|
80
|
-
import { DEFAULT_DENY_REASON, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, } from './hitlBridge.js';
|
|
80
|
+
import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, } 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';
|
|
@@ -305,7 +305,10 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
305
305
|
// binding 不匹配意味着「人看见的那一行在他决断期间被换掉了」,自动重试等于替人对一件
|
|
306
306
|
// 他没看过的事按了 Yes。`no_pending` 同族(那一行已经没了,重试同样只会再失败一次)。
|
|
307
307
|
// 上抛给外层 catch ⇒ typed `failed` ⇒ 调用方走 fail-soft 诚实红,由人重新决断。
|
|
308
|
-
|
|
308
|
+
// Inkglow-1085 P0a:瞬断耗尽同样**不许**回退纯 approve —— 引擎此刻根本够不着,再补一发
|
|
309
|
+
// 纯 approve 只是再烧一轮超时,还把「传输断了」错标成「老 server 不识别 remember」。
|
|
310
|
+
// 上抛给外层 catch ⇒ retryExhausted 判别位 ⇒ parkResolver 走重呈臂。
|
|
311
|
+
if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
|
|
309
312
|
throw e;
|
|
310
313
|
hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
|
|
311
314
|
}
|
|
@@ -314,7 +317,13 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
314
317
|
return { kind: 'decided', gatedCallId };
|
|
315
318
|
}
|
|
316
319
|
catch (e) {
|
|
317
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
kind: 'failed',
|
|
322
|
+
gatedCallId,
|
|
323
|
+
reason: `decide(approve) failed: ${String(e)}`,
|
|
324
|
+
// Inkglow-1085 P0a:瞬断耗尽的判别位(在场即真)—— 语义失败缺席,reason 字节不变。
|
|
325
|
+
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
326
|
+
};
|
|
318
327
|
}
|
|
319
328
|
case 'deny':
|
|
320
329
|
try {
|
|
@@ -326,7 +335,12 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
326
335
|
return { kind: 'decided', gatedCallId, denied: true };
|
|
327
336
|
}
|
|
328
337
|
catch (e) {
|
|
329
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
kind: 'failed',
|
|
340
|
+
gatedCallId,
|
|
341
|
+
reason: `decide(deny) failed: ${String(e)}`,
|
|
342
|
+
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
343
|
+
};
|
|
330
344
|
}
|
|
331
345
|
}
|
|
332
346
|
}
|
|
@@ -403,8 +417,28 @@ function isToolApprovalDelegation(v) {
|
|
|
403
417
|
return false;
|
|
404
418
|
return d.agentName === undefined || typeof d.agentName === 'string';
|
|
405
419
|
}
|
|
406
|
-
/**
|
|
407
|
-
*
|
|
420
|
+
/**
|
|
421
|
+
* 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
422
|
+
* (server 1.258 [1549]①3,旧代际兼容)。
|
|
423
|
+
*
|
|
424
|
+
* 🔴 **`=== true` 是在场判别,不是布尔求值**(#307 S47 复核结论,2026-08-19)。`fromSubagent`
|
|
425
|
+
* 的 wire 形是 `?: true`(见上方声明),所以运行期只可能是 `true` 或缺席;一个**显式 `false`**
|
|
426
|
+
* 只能来自注入面或不合契约的实现,它承载的信息是「这个载体不合契约」,**不是**「引擎明确判定不是
|
|
427
|
+
* 子代」。故这里刻意**不**把 `false` 当成否定证据:它与缺席同档 —— 落回 `sourceTaskId` 在场性
|
|
428
|
+
* 那条旧代际权宜臂。
|
|
429
|
+
*
|
|
430
|
+
* 为什么这个方向是对的(而不是「显式 false ⇒ 直接 return false」):
|
|
431
|
+
* · 本判据的**唯一消费面是展示归属**(`workerBadge` 徽章 + `delegation` 出处链,见下方卡口),
|
|
432
|
+
* 不参与任何放行/收窄决策 —— 两个方向的代价不对称:多一枚徽章只是噪声,少一枚徽章是把
|
|
433
|
+
* 「这是子代发起的」这条事实对用户藏起来;
|
|
434
|
+
* · `sourceTaskId` 本身就是 server 只对后台子代 lane 才出的键(见其声明),它在场是**独立的**
|
|
435
|
+
* 子代证据。让一个不合契约的 `false` 去否决一条独立成立的证据,等于让注入面拿到一个
|
|
436
|
+
* 「隐藏子代身份」的开关;
|
|
437
|
+
* · 与本文件 `governanceForced` 的「缺席 ≠ false」同族纪律:在场即真的键上,非 `true` 一律读作
|
|
438
|
+
* 「没有这条证据」,而不是「有一条相反的证据」。
|
|
439
|
+
* 反漂移:类型侧 `?: true` 已让**编译期**的显式 false 不可能构造;本臂守的是运行期(wire/注入面)
|
|
440
|
+
* 的越界载体,pure 门有对应负控(`fromSubagent:false` + sourceTaskId 在场 ⇒ 仍判子代)。
|
|
441
|
+
*/
|
|
408
442
|
export function isFromSubagent(frame) {
|
|
409
443
|
if (frame.fromSubagent === true)
|
|
410
444
|
return true;
|
package/dist/index.d.ts
CHANGED
|
@@ -246,6 +246,7 @@ export * from './model/catalogLoader.js';
|
|
|
246
246
|
export * from './model/providerAuth.js';
|
|
247
247
|
export * from './model/providerCatalog.js';
|
|
248
248
|
export * from './model/tierVocabulary.js';
|
|
249
|
+
export * from './model/modelSupplyRules.js';
|
|
249
250
|
export * from './websearch/searchProviderPresets.js';
|
|
250
251
|
export * from './env/localeGeo.js';
|
|
251
252
|
export * from './env/localeTag.js';
|
package/dist/index.js
CHANGED
|
@@ -428,6 +428,10 @@ export * from './model/providerCatalog.js';
|
|
|
428
428
|
// 🔴 `model/tierVocabulary.ts` = 档位五档词表 + CC 别名 + isTier 校验 + resolveTierBinding
|
|
429
429
|
// fail-open 降档派生。此前跨两仓三份;settings 读写那半场仍归宿主(cli tierCore 只留存储)。
|
|
430
430
|
export * from './model/tierVocabulary.js';
|
|
431
|
+
// 🔴 `model/modelSupplyRules.ts` = **Model Hub 供给面的三端公共判定**(#318 件③ 上收,cli [4752]
|
|
432
|
+
// 预告的三纯函数):vision 生效值+来源三态 / 删除断链核(拒删)/ 删除降级后果(照删但必说)。
|
|
433
|
+
// 零 IO —— 读盘那半场留各端(壳 modelChannels 读完再调这里);`doc === null` 的两义由调用方分流。
|
|
434
|
+
export * from './model/modelSupplyRules.js';
|
|
431
435
|
// ── 搜索 provider 目录 + 地域预选(2026-07-31)──────────────────────────────────────────────────
|
|
432
436
|
// `websearch/searchProviderPresets` = model 目录的**同形不同表**姊妹件:数据在 json、类型与查询
|
|
433
437
|
// 在 ts。它编译出来的不是模型目录而是**引擎部署 env**(`WEB_SEARCH_*`)。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** vision 生效值的**出处**(UI 那一行「当前生效值与来源」的唯一数据源)。 */
|
|
2
|
+
export type VisionSource =
|
|
3
|
+
/** 这条 entry 自己显式表态。 */
|
|
4
|
+
'entry'
|
|
5
|
+
/** entry 不表态,取 `inferFamily` 的家族缺省。 */
|
|
6
|
+
| 'family'
|
|
7
|
+
/** 两边都不表态 —— 生效值**未知**,绝不折算成 false。 */
|
|
8
|
+
| 'unknown';
|
|
9
|
+
/**
|
|
10
|
+
* {@link resolveEntryVision} 的结果(具名:本仓 typeshape 门对导出面内联匿名形有棘轮)。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 `value: undefined` 与 `value: false` 是**两件事**:前者「没人说过这个模型能不能看图」,
|
|
13
|
+
* 后者「确认它不能看图」。把前者渲成后者会永久封死一个真能力。
|
|
14
|
+
*/
|
|
15
|
+
export interface EntryVisionResolution {
|
|
16
|
+
value: boolean | undefined;
|
|
17
|
+
source: VisionSource;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 解析一条 entry 的 vision **生效值 + 来源**。纯函数(只吃 modelId/vision 两位,零 IO)。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 [honest-absence]:家族与 entry 都不表态时给 `undefined`(未知),**不**给 `false`。
|
|
23
|
+
* ⚠️ 「未知」只是**客户端这一层**诚实:引擎侧 registry-core `ModelEntry.vision` 是
|
|
24
|
+
* `z.boolean().default(false)`,键缺席在那边就等于 false。**呈现层必须把这个后果一起说出来**,
|
|
25
|
+
* 别让用户以为「未知」= 引擎会自己去问供应商。
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveEntryVision(entry: {
|
|
28
|
+
modelId: string;
|
|
29
|
+
vision?: boolean;
|
|
30
|
+
}): EntryVisionResolution;
|
|
31
|
+
/** 一处挡住删除的引用。`where` 是坐标(用户照着去改),`fix` 是这一处该怎么解开。 */
|
|
32
|
+
export type PoolDeleteBlocker = {
|
|
33
|
+
kind: 'default' | 'roles' | 'roster' | 'unreadable';
|
|
34
|
+
where: string;
|
|
35
|
+
fix: string;
|
|
36
|
+
};
|
|
37
|
+
/** 一条「照删,但你该知道」的后果。删除**不**被阻断,确认屏必须把它渲出来。 */
|
|
38
|
+
export type PoolDeleteWarning = {
|
|
39
|
+
kind: 'allowlist-empties' | 'tier-binding';
|
|
40
|
+
/** 人话:删了会发生什么(不是坐标,是后果)。 */
|
|
41
|
+
consequence: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 档位组的**归一形**(settings `semaTierGroups`)。两处删除判定共用,单独具名以免两边各写一份内联形。
|
|
45
|
+
*/
|
|
46
|
+
export interface TierGroupsView {
|
|
47
|
+
active: string;
|
|
48
|
+
groups: Record<string, {
|
|
49
|
+
tiers: Record<string, string | undefined>;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/** {@link computeDeleteWarnings} 的入参(具名:导出面禁内联匿名形,B4 棘轮)。 */
|
|
53
|
+
export interface DeleteWarningsInput {
|
|
54
|
+
id: string;
|
|
55
|
+
/**
|
|
56
|
+
* 已读到的 models.json 文档切片。🔴 两位是 `unknown` 而不是收窄形 —— 它们是**盘上 JSON 的原样**,
|
|
57
|
+
* 属主是引擎的 models 域 schema(开集、可演进)。在这里手抄一份结构就是手抄一份会漂的上游形;
|
|
58
|
+
* 本函数对它们的处理本来就是「只判是不是数组/对象,然后逐项过滤」,窄读没有消费者。
|
|
59
|
+
* `null` = 调用方没读到文档(两义分流在调用方,见 {@link computeDeleteBlockers} 头注)。
|
|
60
|
+
*/
|
|
61
|
+
doc: {
|
|
62
|
+
atModelAllowlist?: unknown;
|
|
63
|
+
tierGroups?: unknown;
|
|
64
|
+
} | null;
|
|
65
|
+
/** settings `semaTierGroups` 归一形(缺席 ⇒ 回落 `doc.tierGroups`,与写路径的取值序同源)。 */
|
|
66
|
+
tierGroups: TierGroupsView | null;
|
|
67
|
+
/** 删除**之后**仍在池里的 catalog name 全集(判「有效名单会不会变空」要的就是它)。 */
|
|
68
|
+
survivingIds: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
/** {@link computeDeleteBlockers} 的入参(具名:同上)。 */
|
|
71
|
+
export interface DeleteBlockersInput {
|
|
72
|
+
id: string;
|
|
73
|
+
/** 已读到的 models.json 文档切片;两位 `unknown` 的理由同 {@link DeleteWarningsInput.doc}。 */
|
|
74
|
+
doc: {
|
|
75
|
+
default?: unknown;
|
|
76
|
+
roles?: unknown;
|
|
77
|
+
} | null;
|
|
78
|
+
/** roster 归一形(激活组名 + 全部组的 slot 表;**分阶段组也在里面**,它们是真引用)。 */
|
|
79
|
+
rosters: {
|
|
80
|
+
active: string;
|
|
81
|
+
rosters: Record<string, {
|
|
82
|
+
slots: Record<string, string>;
|
|
83
|
+
}>;
|
|
84
|
+
} | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 纯判定:删这条 entry 会带来哪些**语义变化**(不阻断)。
|
|
88
|
+
*
|
|
89
|
+
* 两格,都是「链不断但意思变了」:
|
|
90
|
+
* ① `atModelAllowlist` 的**有效名单**会被删空 ⇒ 按 registry-core 语义**全部启用模型都可 @**
|
|
91
|
+
* —— 一次静默的权限放宽,必须说;名单里还有别人 ⇒ 只是缩小名单,不用说。
|
|
92
|
+
* ② `semaTierGroups` 里有绑定指着它 ⇒ 删完那条绑定悬空 ⇒ 下次按档位解析会落到**另一个模型**上
|
|
93
|
+
* (而不是报错),用户会以为「我明明配了 pro 档」。
|
|
94
|
+
*/
|
|
95
|
+
export declare function computeDeleteWarnings(input: DeleteWarningsInput): PoolDeleteWarning[];
|
|
96
|
+
/**
|
|
97
|
+
* 纯判定:这条 entry 现在被哪些**断链类**引用挡着。空数组 = 可以删。
|
|
98
|
+
*
|
|
99
|
+
* 🔴 `doc === null` 的两义**在调用方分流**:这里只认「已读到的文档」;读不出来时调用方自己产
|
|
100
|
+
* `unreadable` 阻断 —— 那一刻我们并不知道谁指着它,fail-closed 拒删比「猜没人指着」安全。
|
|
101
|
+
*/
|
|
102
|
+
export declare function computeDeleteBlockers(input: DeleteBlockersInput): PoolDeleteBlocker[];
|