@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
|
@@ -125,7 +125,8 @@ export const CLAIM_HELD_STATES = ['running', 'suspended', 'needs_review'];
|
|
|
125
125
|
* 🔴 判据锚在「**有没有一张现在就能答、答了就放行的卡**」这个决定量上,不是锚在「是不是 park
|
|
126
126
|
* 态」这个前置条件上([anchor-on-the-deciding-quantity])。所以三张 reopen-failed / not-parked /
|
|
127
127
|
* state-unknown 全部落 `not-delivered`:它们同样是 park,但卡没能呈到用户面前,把注入件放回队列
|
|
128
|
-
* 只会在下一拍再撞一次同样的 409
|
|
128
|
+
* 只会在下一拍再撞一次同样的 409。`running-not-found`(幽灵 claim)同落 `not-delivered`:
|
|
129
|
+
* 「已解锁」证不出,自动重投可能撞回仍锁着的会话。
|
|
129
130
|
*/
|
|
130
131
|
export function selfHealSubmissionDisposition(outcome) {
|
|
131
132
|
switch (outcome.kind) {
|
|
@@ -202,6 +203,37 @@ export function readSteerReceiptStatus(receipt) {
|
|
|
202
203
|
}
|
|
203
204
|
/** cancel 之后**有界**等那条 run 交出会话的缺省窗(`POST …/cancel` 是 202 异步 —— 收下 ≠ 已停)。 */
|
|
204
205
|
export const CANCEL_RELEASE_WAIT_MS = 10_000;
|
|
206
|
+
/** 出卡前存活对账那一发 `runs.get` 的等待上界(Inkglow-1085 P0b②)——与假死锁复核读口同款
|
|
207
|
+
* 「慢网也回得来」的诚实预算;窗尽/传输错 = 读不到 ⇒ 保守维持出卡(见 runningChoiceArm 注)。 */
|
|
208
|
+
const RUNNING_LIVENESS_RECHECK_TIMEOUT_MS = 4_000;
|
|
209
|
+
// ── running 三选卡的「已呈现且用户选了 Do nothing」登记(Inkglow-1085 P0b①,2026-08-19)────────
|
|
210
|
+
//
|
|
211
|
+
// 病形(案卷车2 发现2):同一条幽灵/长跑 run 占着会话时,用户每发一条消息就被整卡打断一次 ——
|
|
212
|
+
// 呈现台账只有 per-call 局部量(offeringByTask / runningChoiceOffered),没有任何跨 turn 的
|
|
213
|
+
// 「用户已对 run X 说过『别动它』」记账。本登记 = 模块级 (sessionKey,taskId) 集合:
|
|
214
|
+
// · 只在**卡真呈现过且用户选了 wait** 时登记(呈卡腿抛错那次不登记 —— 用户没看到卡,压掉后续
|
|
215
|
+
// 等于把没呈过的卡当已呈);
|
|
216
|
+
// · 登记在场 ⇒ 同 (sessionKey,taskId) 再撞 409 不再整卡重弹,结局带 `alreadyOffered: true`
|
|
217
|
+
// 判别位(端据此降级渲一行;默认文案层已给降级行,含 cancel 端点与 wayOut 两条真出路);
|
|
218
|
+
// · 用户选 steer/cancel ⇒ 清登记(他重新对这条 run 表了态);宿主的「重新打开操作菜单」入口
|
|
219
|
+
// 走 {@link clearRunningChoiceOffer}。
|
|
220
|
+
// run 身份变化天然换键(taskId 不同);同 run 状态迁移到 park 态会走别的臂,登记不拦。
|
|
221
|
+
const runningChoiceDeclined = new Set();
|
|
222
|
+
/** 登记表上限(codex 复审轻形采纳):超限 FIFO 驱逐最老条目 —— 驱逐代价 = 卡重弹一次(修前行为)。 */
|
|
223
|
+
const RUNNING_CHOICE_LEDGER_CAP = 512;
|
|
224
|
+
function runningChoiceDeclineKey(sessionId, taskId) {
|
|
225
|
+
// NUL 分隔:两段都是外来串,可打印分隔符在「sessionId 里恰有它」时会串键。
|
|
226
|
+
return `${sessionId ?? ''}\u0000${taskId}`;
|
|
227
|
+
}
|
|
228
|
+
/** 清掉某条 run 的「Do nothing」登记 —— 宿主「重新打开操作菜单」入口(下一次 409 重新整卡呈现)。
|
|
229
|
+
* `sessionId` 与当时喂给 {@link ActiveRunSelfHealDeps.sessionId} 的值同源(缺席 = 默认键)。 */
|
|
230
|
+
export function clearRunningChoiceOffer(taskId, sessionId) {
|
|
231
|
+
runningChoiceDeclined.delete(runningChoiceDeclineKey(sessionId, taskId));
|
|
232
|
+
}
|
|
233
|
+
/** 测试钩:清空整张登记表(套件各组之间隔离用)。 */
|
|
234
|
+
export function __resetRunningChoiceLedgerForTests() {
|
|
235
|
+
runningChoiceDeclined.clear();
|
|
236
|
+
}
|
|
205
237
|
/** 假死锁复核读口(`deps.listOwnedPendingApprovals`)的等待上界:一发 approvals 列表读,4s 是
|
|
206
238
|
* 「慢网也回得来」的诚实预算;窗尽=分不出真卡与幽灵 ⇒ 保守 stand down(见调用点注)。 */
|
|
207
239
|
const OWNED_PENDING_RECHECK_TIMEOUT_MS = 4_000;
|
|
@@ -419,8 +451,12 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
|
|
|
419
451
|
return askParkArm(taskId, signal, deps);
|
|
420
452
|
// 真在跑 ⇒ 把引擎给着的两条路(steer / cancel)+ 现状做成三选卡交给用户;表外的其它状态
|
|
421
453
|
// (含今天还不存在的)照旧「不动它 + 如实说」—— 语义不明的状态不配递把手(见 RUNNING_STATES 注)。
|
|
422
|
-
|
|
423
|
-
|
|
454
|
+
// Inkglow-1085 P0b②:`statusFromWire` = 这个 running 是 409 终帧直供的投影(best-effort,可陈旧
|
|
455
|
+
// ——claim/store 错时恒回送 running 的幽灵读数);出卡前 runningChoiceArm 会对它补一发 runs.get
|
|
456
|
+
// 真查。status 本就是上面刚 get 来的(wire 缺席回退)⇒ 已新鲜,不打第二发。
|
|
457
|
+
if (RUNNING_STATES.includes(status)) {
|
|
458
|
+
return runningChoiceArm(taskId, status, signal, signal.activeTaskStatus !== null, runs, deps);
|
|
459
|
+
}
|
|
424
460
|
return { kind: 'not-parked', taskId, status };
|
|
425
461
|
}
|
|
426
462
|
/** plan_review 重开口的判决(臂与判决分开:steer 的 `queued` 回执要的是**同一份**重开语义的判决值,
|
|
@@ -465,7 +501,7 @@ async function askParkArm(taskId, signal, deps) {
|
|
|
465
501
|
* ③ 选项按**供给**渲(verb 缺席 = 那条路兑现不了 = 整个不渲),cancel 还额外要 `get` ——
|
|
466
502
|
* 没有它就确认不了 claim 真的释放,「取消并重发」就成了猜。
|
|
467
503
|
*/
|
|
468
|
-
async function runningChoiceArm(taskId, status, runs, deps) {
|
|
504
|
+
async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps) {
|
|
469
505
|
const notParked = { kind: 'not-parked', taskId, status };
|
|
470
506
|
const offer = deps?.offerRunningChoice;
|
|
471
507
|
if (typeof offer !== 'function')
|
|
@@ -480,13 +516,92 @@ async function runningChoiceArm(taskId, status, runs, deps) {
|
|
|
480
516
|
const canCancel = typeof runs?.cancel === 'function' && typeof runs?.get === 'function';
|
|
481
517
|
if (!canSteer && !canCancel)
|
|
482
518
|
return notParked;
|
|
519
|
+
// ── 存活对账腿(Inkglow-1085 P0b②):出卡前对 wire 直供的 running 补一发 runs.get 真查 ────────
|
|
520
|
+
// 409 终帧的 `activeTaskStatus` 是引擎 claim/store 的 best-effort 投影 —— claim 指着一条早已终结
|
|
521
|
+
// 甚至已不存在的 run 时,它会**每次都**回送同一个错的 running(幽灵),用户按 wire 读数拿到的
|
|
522
|
+
// steer/cancel 两条路全是死路。
|
|
523
|
+
// 🔴 [4664] server 定谳(2026-08-19)把「别把 409 running 当绝对真值」写成了四条成文的洞:
|
|
524
|
+
// ①park 态不在 reapStale 射程;②`updated_at` 误杀窗;③409 与 poll 口径短暂相左;
|
|
525
|
+
// ④LOCAL(file)车道无周期自愈腿。SQL 车道死 run 的自愈上界 ≈180s(心跳 30s +
|
|
526
|
+
// REAP_RUN_STALE_SEC 120 + tick 60)——**LOCAL 车道无上界,勿假设 180s**,所以本腿不做
|
|
527
|
+
// 「等它自愈」的时间性假设,只做一手读面复核。
|
|
528
|
+
// 真源两级里它是二手货,出卡(把把手递给人)前用一手读面复核一次:
|
|
529
|
+
// · get 读回非 running 的**已知**词 ⇒ wire 读数过期,按真态走(park 词进对应重开臂,其余落
|
|
530
|
+
// not-parked 如实说)—— 与「kind 缺席 ⇒ status 表回退」同一张表,不新铸分臂;
|
|
531
|
+
// · get 撞 404 ⇒ 幽灵 claim(`running-not-found` 判别位;为什么不算「已释放」见该臂注);
|
|
532
|
+
// · get 网络失败 / 本发截止 / 奇形记录 ⇒ **读不到 ≠ 非 running**,保守维持出卡 —— 误关卡会把
|
|
533
|
+
// steer/cancel 两条真把手一起藏掉;
|
|
534
|
+
// · status 本就是本层刚 runs.get 查来的(statusFromWire=false)⇒ 已新鲜,不打第二发。
|
|
535
|
+
// 🔴 经函数读 aborted(waitForClaimRelease.isAborted 同注):它在 await 两侧会变,直接读两次会被
|
|
536
|
+
// tsc 控制流分析把第二次窄成恒假比较(TS2367)。
|
|
537
|
+
const probeCallerAborted = () => deps?.signal?.aborted === true;
|
|
538
|
+
if (statusFromWire && typeof runs?.get === 'function' && !probeCallerAborted()) {
|
|
539
|
+
let fresh = null;
|
|
540
|
+
let probeAnswered = false;
|
|
541
|
+
let ghost = false;
|
|
542
|
+
const lease = claimProbeLease(deps?.signal, RUNNING_LIVENESS_RECHECK_TIMEOUT_MS);
|
|
543
|
+
try {
|
|
544
|
+
fresh = readStatus(await runs.get(taskId, { signal: lease.signal, ...sessionOpts(deps) }));
|
|
545
|
+
probeAnswered = true;
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
if (e?.status === 404)
|
|
549
|
+
ghost = true;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
lease.release();
|
|
553
|
+
}
|
|
554
|
+
if (ghost)
|
|
555
|
+
return { kind: 'running-not-found', taskId };
|
|
556
|
+
// (codex 复审 [medium] 采纳)对账期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记)都不做 ——
|
|
557
|
+
// Esc 之后还弹卡/重开 = 用户说停还在动。零动作现状行收口(与呈卡腿抛错同一条诚实收口)。
|
|
558
|
+
if (probeCallerAborted())
|
|
559
|
+
return notParked;
|
|
560
|
+
if (probeAnswered && fresh !== null && !RUNNING_STATES.includes(fresh)) {
|
|
561
|
+
if (PLAN_REVIEW_STATES.includes(fresh))
|
|
562
|
+
return planReviewArm(taskId, busy, deps);
|
|
563
|
+
if (ASK_PARK_STATES.includes(fresh))
|
|
564
|
+
return askParkArm(taskId, busy, deps);
|
|
565
|
+
// (codex 复审 [high] 采纳)终态词 ⇒ 专属结局:not-parked 的「wait for it to finish」对一条
|
|
566
|
+
// 已终结的 run 是永远等不到的假话。表外的未知词仍落 not-parked 如实说(不替引擎断言终结)。
|
|
567
|
+
if (CLAIM_RELEASED_STATES.includes(fresh))
|
|
568
|
+
return { kind: 'running-settled', taskId, status: fresh };
|
|
569
|
+
return { kind: 'not-parked', taskId, status: fresh };
|
|
570
|
+
}
|
|
571
|
+
// 确认仍 running(或读不到)⇒ 按原判继续出卡。
|
|
572
|
+
}
|
|
573
|
+
// ── 已呈现登记(Inkglow-1085 P0b①):同 (sessionKey,taskId) 用户选过「Do nothing」⇒ 不再整卡
|
|
574
|
+
// 重弹,给 `alreadyOffered` 判别位让端降级渲一行(登记语义与清口见 runningChoiceDeclined 顶注)。
|
|
575
|
+
const declineKey = runningChoiceDeclineKey(deps?.sessionId, taskId);
|
|
576
|
+
if (runningChoiceDeclined.has(declineKey)) {
|
|
577
|
+
return { kind: 'not-parked', taskId, status, alreadyOffered: true };
|
|
578
|
+
}
|
|
483
579
|
let choice = 'wait';
|
|
580
|
+
let offerDelivered = false;
|
|
484
581
|
try {
|
|
485
582
|
choice = (await offer({ taskId, status, canSteer, canCancel })) ?? 'wait';
|
|
583
|
+
offerDelivered = true;
|
|
486
584
|
}
|
|
487
585
|
catch {
|
|
488
586
|
choice = 'wait'; // 呈卡腿出意外 = 用户没做决定 ⇒ 零动作(绝不掉进破坏性分支)
|
|
489
587
|
}
|
|
588
|
+
// wait = 用户看过卡并选择留着它(呈现层把 Esc/空答也收口成 wait —— 同样是「这次不动它」的表态);
|
|
589
|
+
// steer/cancel = 用户重新表了态 ⇒ 清登记。呈卡腿抛错那次**不**登记(用户没看到卡)。
|
|
590
|
+
if (choice === 'wait') {
|
|
591
|
+
if (offerDelivered) {
|
|
592
|
+
runningChoiceDeclined.add(declineKey);
|
|
593
|
+
// (codex 复审 [medium] 轻形采纳)FIFO 有界:长命桌面/多会话宿主不许无界长住。驱逐最老条目的
|
|
594
|
+
// 代价 = 那条 run 的卡重弹一次(= 修前行为,方向安全);Set 按插入序迭代,首项即最老。
|
|
595
|
+
if (runningChoiceDeclined.size > RUNNING_CHOICE_LEDGER_CAP) {
|
|
596
|
+
const oldest = runningChoiceDeclined.values().next().value;
|
|
597
|
+
if (oldest !== undefined)
|
|
598
|
+
runningChoiceDeclined.delete(oldest);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
runningChoiceDeclined.delete(declineKey);
|
|
604
|
+
}
|
|
490
605
|
if (choice === 'steer' && canSteer && runs?.steer !== undefined) {
|
|
491
606
|
try {
|
|
492
607
|
// 🔴 正文原样送(untrusted DATA,server 侧围栏);恰一次,绝不重试。方法形调用保接收者。
|
|
@@ -621,6 +736,19 @@ function governanceOriginClause(signal) {
|
|
|
621
736
|
? ' That gate is enforced by this deployment\'s governance policy, not by a permission rule you set.'
|
|
622
737
|
: '';
|
|
623
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* 活性证据(Inkglow-1085 P0b,[4664]:409 体内 `msSinceLastActivity` 是唯一可消费的活性证据位)。
|
|
741
|
+
* 🔴 只在位真在场时说话(缺席一个字都不说 —— 缺席 = 无法证明,不是「没有活动」);它是**展示**
|
|
742
|
+
* 判别,不改变任何动作。只挂在 running 形的 not-parked 行上:那正是「它到底还活着吗」这个问题
|
|
743
|
+
* 被问出来的地方。
|
|
744
|
+
*/
|
|
745
|
+
function lastActivityClause(signal) {
|
|
746
|
+
const ms = signal?.msSinceLastActivity;
|
|
747
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
|
|
748
|
+
return '';
|
|
749
|
+
const human = ms < 120_000 ? `${String(Math.max(1, Math.round(ms / 1000)))}s` : `${String(Math.round(ms / 60_000))}m`;
|
|
750
|
+
return ` The engine last recorded activity on that run ${human} ago.`;
|
|
751
|
+
}
|
|
624
752
|
/**
|
|
625
753
|
* 每种结局的上屏整行。**每一句出路都真的接了线**;没有真出路的分支就直说「这个会话暂时无法
|
|
626
754
|
* 继续」。重开成功那条也要上屏 —— 卡是替用户重新打开的,他得知道去答哪张、答完做什么。
|
|
@@ -638,7 +766,9 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin) {
|
|
|
638
766
|
const base = activeRunSelfHealBaseRow(outcome, signal, copy?.wayOut ?? DEFAULT_WAY_OUT);
|
|
639
767
|
switch (outcome.kind) {
|
|
640
768
|
// 这两条是「用户此刻卡住了、而且没有别的把手」的结局 —— wire 给的 decide 入口在这里才有用。
|
|
769
|
+
// not-parked(running 形)另挂活性证据([4664] 判别位):「它到底还活着吗」在这一行被问出来。
|
|
641
770
|
case 'not-parked':
|
|
771
|
+
return base + lastActivityClause(signal) + decidePathClause(signal) + governanceOriginClause(signal);
|
|
642
772
|
case 'state-unknown':
|
|
643
773
|
return base + decidePathClause(signal) + governanceOriginClause(signal);
|
|
644
774
|
// 其余结局手上都另有一张更好的把手(重开的卡 / 已开的卡 / 失败臂基句自己渲的 decidePath)
|
|
@@ -822,7 +952,30 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
|
|
|
822
952
|
return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), so ` +
|
|
823
953
|
`nothing was cancelled and sema cannot tell whether that run is still holding this session. Your message ` +
|
|
824
954
|
`was NOT sent; send it again to find out, or ${wayOut}.`);
|
|
955
|
+
case 'running-not-found':
|
|
956
|
+
// Inkglow-1085 P0b②:幽灵 claim —— 引擎一边说这条 run 占着会话、一边查无此 run。「已解锁」
|
|
957
|
+
// 证不出(404 与 non-owner 同码刻意不可分辨),所以只说已证的事实 + 「再发一次」这条真出路
|
|
958
|
+
// (与 running-cancel-timeout 读不出那格同款措辞方向:真没了就会直接跑起来)。
|
|
959
|
+
return (`The engine reported this session as held by run ${outcome.taskId} (status running), but it no ` +
|
|
960
|
+
`longer recognizes that run when asked directly — that claim looks stale (the run most likely no ` +
|
|
961
|
+
`longer exists). sema did not offer to steer or cancel it: there is nothing left to act on. ` +
|
|
962
|
+
`Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
|
|
963
|
+
case 'running-settled':
|
|
964
|
+
// codex 复审 [high] 采纳:run 已终结而 claim 仍报 running([4664] 洞③的窗口形)——
|
|
965
|
+
// 「wait for it to finish」对它是永远等不到的假话;真出路 = 再发一次(claim 落地即跑)。
|
|
966
|
+
return (`Run ${outcome.taskId} has already finished (status ${outcome.status}), but the engine still ` +
|
|
967
|
+
`reported it as holding this session — that claim looks stale or mid-release. sema did not offer ` +
|
|
968
|
+
`to steer or cancel it: the run is over. Your message was NOT sent; send it again (once the claim ` +
|
|
969
|
+
`clears it will just run), or ${wayOut}.`);
|
|
825
970
|
case 'not-parked':
|
|
971
|
+
// Inkglow-1085 P0b①:同一条 run 的三选卡已呈现过且用户选了「Do nothing」⇒ 降级成一行,
|
|
972
|
+
// 不再整卡打断;两条真出路(引擎 cancel 端点 / wayOut)必须还在这一行里 —— 压噪不压把手。
|
|
973
|
+
if (outcome.alreadyOffered === true) {
|
|
974
|
+
return (`Run ${outcome.taskId} is still holding this session (status ${outcome.status}). sema already ` +
|
|
975
|
+
`asked what to do with it and you chose to leave it alone, so it is not asking again. ` +
|
|
976
|
+
`Your message was NOT sent; wait for that run, cancel it on the engine ` +
|
|
977
|
+
`(POST /v1/runs/${outcome.taskId}/cancel), or ${wayOut}.`);
|
|
978
|
+
}
|
|
826
979
|
// 🔴 「等它跑完」这句只在**它真的在跑**的时候是真话。分诊表外的门/状态里也可能是 park
|
|
827
980
|
// (表外 gate kind 正是走这条),而 wire 恰恰会在那种情况下带 pendingGate —— 对一个
|
|
828
981
|
// parked run 说「wait for it to finish」是一句永远等不到的假出路。动作仍然一个字不改
|
|
@@ -42,6 +42,8 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
|
|
|
42
42
|
'prompt_suggestions',
|
|
43
43
|
'human_input',
|
|
44
44
|
'turn_usage',
|
|
45
|
+
// #310 / #318 件①:引擎结构化通告的会话面(raw 预分派铸点,见 eventToSdkMessage 顶部)。
|
|
46
|
+
'engine_notice',
|
|
45
47
|
]);
|
|
46
48
|
/** Wrap neutral content blocks in the CC `assistant` message envelope. */
|
|
47
49
|
function assistantArm(ctx, content) {
|
|
@@ -87,6 +89,24 @@ function assertNeverArm(_ev) {
|
|
|
87
89
|
* 旧写法 `if (msg)` 在新返回型上恒真(对象永远 truthy),所以这是**必须点名**的一类改动。
|
|
88
90
|
*/
|
|
89
91
|
export function eventToSdkMessage(ev, ctx) {
|
|
92
|
+
// ── `engine_notice` raw 预分派(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)──
|
|
93
|
+
//
|
|
94
|
+
// 🔴 **为什么是 raw 预分派而不是一条 `case`**(与 `workflow_complete` / `human_input` 当年同因):
|
|
95
|
+
// 本臂**还没进已发布 SDK 的 `AgentEvent` union**。sdk 仓 `3d6aebc` 确实加了它,但那个 commit
|
|
96
|
+
// **尚未出包** —— 亲验 npm `@sema-agent/sdk@7.2.0`(latest,2026-08-16 发布)的真 tarball:
|
|
97
|
+
// `dist/` 全树零 `engine_notice`(而同批的 `FleetTaskRow.cycleSeq` 在,证明抽检会说话)。
|
|
98
|
+
// 在这样的 union 上写 `case 'engine_notice'` 是编译错,所以先走预分派。
|
|
99
|
+
// 🔴 **这不是「按源码将就接」**(接入文档宪法):消费契约取自 server 的**已发布**接入档
|
|
100
|
+
// (ASSISTANT-WIRE-CONTRACT 附录 D,server 7.36+)与 openapi `Event_engine_notice`,不是抄 sdk src。
|
|
101
|
+
// 档与实装的失真已如实记账(附录 D.3 仍写「起步白名单三码」,而 server main 的白名单已是六码 ——
|
|
102
|
+
// `memory.hold_opened` / `hold_released` / `hold_disposed` 随 core 5.47/5.48 的 `NOTICE_AUDIENCE`
|
|
103
|
+
// 入册)。**本层对此完全免疫**:白名单是 server 的投递判定,本层按开集消费,一个码都不硬编。
|
|
104
|
+
// 🔴 **到期复核(自退休,不靠人记)**:预分派用 `(ev as {type?:unknown})` 形读判别键,**不收窄** `ev`
|
|
105
|
+
// ⇒ 臂一进 union,switch 的 `default` 仍看得见它,B5 穷举断言 `assertNeverArm` **编译期真红**,
|
|
106
|
+
// 逼下一棒把它搬进 switch。搬进去时行为一字不改(下面的投影函数原样复用)。
|
|
107
|
+
if (ev.type === 'engine_notice') {
|
|
108
|
+
return engineNoticeProjection(ev, ctx);
|
|
109
|
+
}
|
|
90
110
|
switch (ev.type) {
|
|
91
111
|
// ── `human_input`(core 5.14.0 design/171 / server 7.4.0 SSE,[3017]/[3020])────────────
|
|
92
112
|
// 🔴 **到期复核已兑现(sdk 6.9.0 提货,2026-08-08)**:本臂此前是 switch **之前**的一条 raw
|
|
@@ -268,11 +288,15 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
268
288
|
// phase/detail/retryInSec(SDK 面尚未跟上 core)。类型面缺席不等于 wire 上缺席 —— 按
|
|
269
289
|
// unknown 读、按 number 窄化后原样透传,否则引擎真发的量在这一层就被剥掉了。
|
|
270
290
|
// 键集真源 = retryStatus.ts 的 `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对账)。
|
|
291
|
+
// #307 S44(2026-08-19):core 5.43.0 又加了 `errClass`(这次等待的**原因分桶**,
|
|
292
|
+
// provider 中立闭集)—— SDK 的 `status` 臂类型同样还没跟,同款 unknown 读 + 串窄化透传。
|
|
271
293
|
const st = ev;
|
|
272
294
|
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
273
295
|
const attempt = num(st.attempt);
|
|
274
296
|
const maxRetries = num(st.maxRetries);
|
|
275
297
|
const retryInMs = num(st.retryInMs);
|
|
298
|
+
// 非空串才透传(空串既不是桶也不是「不知道」,只会在下游被渲成一个空的原因)。
|
|
299
|
+
const errClass = typeof st.errClass === 'string' && st.errClass.length > 0 ? st.errClass : undefined;
|
|
276
300
|
return projected(stamp(ctx, armBody({
|
|
277
301
|
type: 'retry_status',
|
|
278
302
|
phase: ev.phase,
|
|
@@ -281,6 +305,7 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
281
305
|
...(retryInMs !== undefined ? { retryInMs } : {}),
|
|
282
306
|
...(attempt !== undefined ? { attempt } : {}),
|
|
283
307
|
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
308
|
+
...(errClass !== undefined ? { errClass } : {}),
|
|
284
309
|
// 🔴 §E2 lane 身份必须透传(2026-08-08 对抗复审二轮复审命中的**跨 lane 状态破坏**)。
|
|
285
310
|
// `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
|
|
286
311
|
// `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
|
|
@@ -543,6 +568,80 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
543
568
|
return dropped('unknown_arm', String(ev.type ?? 'unknown'));
|
|
544
569
|
}
|
|
545
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* `engine_notice` → 中性内部通告臂(#310 / #318 件①,契约 = server `ASSISTANT-WIRE-CONTRACT` 附录 D
|
|
573
|
+
* + openapi `Event_engine_notice`)。
|
|
574
|
+
*
|
|
575
|
+
* ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
|
|
576
|
+
* 引擎的**结构化通告**里,被 server 判为面向**本会话终端用户**的那一小撮,按 `sessionId` 路由到
|
|
577
|
+
* 这条会话的流上。live 与 durable **两腿都有**(server 三条 run 腿都挂了口)⇒ 断连重连的重放里
|
|
578
|
+
* 会**再看到它**,与 `workspace_changed` 同一条**幂等消费**纪律(附录 D.1 逐字)。
|
|
579
|
+
*
|
|
580
|
+
* ── 🔴 开集三条(本函数的全部判据,逐条都是「不许做什么」)────────────────────────────────
|
|
581
|
+
* ① **按 `code` + `detail` 消费,`message` 只作 fallback 展示**。core 明写
|
|
582
|
+
* `memory.session_polluted` 的 message 随 `memoryProvenance` 模式变文 ⇒ 按 message 文本匹配
|
|
583
|
+
* **必碎**(5.41 合流码形退役同教训)。所以本层把三者**分别**上臂,绝不把 detail 折进文案。
|
|
584
|
+
* ② **`code` 是开集,认不得也绝不丢帧**。server 的白名单会随 core 码册增长(起步三码 →
|
|
585
|
+
* core 5.47/5.48 的 hold 三码入册,附录 D.3 的表尚未跟上)。本层因此**一个码都不硬编**:
|
|
586
|
+
* 没有识别表、没有 switch、没有「已知才投」——认不认得是**渲染面**的判断,不是投影面的门。
|
|
587
|
+
* ⇒ 本函数对 `code` 唯一的要求是「非空串」(判别键本身缺席才叫畸形)。
|
|
588
|
+
* ③ **能力位不当两值门**:本臂不读任何 caps、不问「引擎支不支持 engine_notice」。帧到了就投影
|
|
589
|
+
* (A-022「到帧即服务」先例)—— 拿一个探测位去 gate 一条**已经到手的事实**,只会在探测未判/
|
|
590
|
+
* 失败时把真事实丢掉。
|
|
591
|
+
*
|
|
592
|
+
* ── 🔴 消费端纪律(写在臂上,因为三端各写一遍必漂)────────────────────────────────────────
|
|
593
|
+
* · `detail` 已过 server 的 `redactSecrets` + 尺寸 bound(自由文本 1000 字符;深 4 / 键 32 /
|
|
594
|
+
* 数组 32),**仍按外部串处理**(呈现面字符处理走消费方自己的单源)。
|
|
595
|
+
* · `ts` 是 **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。
|
|
596
|
+
* · 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
|
|
597
|
+
* core 顶注):两个数各自读、并列呈现,任何减法都会得出一个**不存在的量**。本层原样透传 detail
|
|
598
|
+
* 正是为了让这条纪律只在渲染面兑现一次,而不是被投影层先算一个差值出来。
|
|
599
|
+
* · **本帧的缺席不代表「没发生」**:非白名单码 / 缺 `sessionId` 的通告 server **如实不投**
|
|
600
|
+
* (宁缺席不串台),全族那一份始终在 server 的结构化日志里(运维面)。所以消费端**绝不许**
|
|
601
|
+
* 从「没收到 engine_notice」反推「记忆姿态正常」。
|
|
602
|
+
*
|
|
603
|
+
* ── 畸形判据(fail-closed 方向)────────────────────────────────────────────────────────────
|
|
604
|
+
* `code` 非串/空串 ⇒ `malformed`(判别键都没有的通告,渲出去只是一行没有主语的噪声,而 dropped
|
|
605
|
+
* 至少会经 `reportDroppedFrame` 留痕)。其余四键**各自**按诚实缺席处理:`message` 非串 ⇒ 空串
|
|
606
|
+
* (fallback 位缺席,渲染面据此走纯 code 呈现)、`detail` 非对象 ⇒ 空对象(**不是**丢帧:通告的
|
|
607
|
+
* 承重物是 code,detail 坏了不该连带把「这件事发生过」一起吞掉)、`sessionId` / `ts` 非法 ⇒ 键不
|
|
608
|
+
* stamp(绝不铸 `0` 这种看起来合法的假读数)。
|
|
609
|
+
*/
|
|
610
|
+
function engineNoticeProjection(ev, ctx) {
|
|
611
|
+
const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
|
|
612
|
+
if (code === undefined)
|
|
613
|
+
return dropped('malformed', 'engine_notice');
|
|
614
|
+
const sessionId = typeof ev.sessionId === 'string' && ev.sessionId.length > 0 ? ev.sessionId : undefined;
|
|
615
|
+
const ts = typeof ev.ts === 'number' && Number.isFinite(ev.ts) ? ev.ts : undefined;
|
|
616
|
+
const rawDetail = ev.detail;
|
|
617
|
+
const detail = typeof rawDetail === 'object' && rawDetail !== null && !Array.isArray(rawDetail)
|
|
618
|
+
? rawDetail
|
|
619
|
+
: {};
|
|
620
|
+
return projected(stamp(ctx, armBody({
|
|
621
|
+
type: 'engine_notice',
|
|
622
|
+
code,
|
|
623
|
+
/** 🔴 fallback 展示位,**不是匹配键**(见本函数顶注 ①)。 */
|
|
624
|
+
message: typeof ev.message === 'string' ? ev.message : '',
|
|
625
|
+
/** 🔴 原样透传(禁挑键):`detail` 逐码不同且是**开集**,白名单挑键 = 新码的事实在本层静默蒸发。 */
|
|
626
|
+
detail,
|
|
627
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
628
|
+
...(ts !== undefined ? { ts } : {}),
|
|
629
|
+
// ── 重放身份:两个键、**两个不同的命名空间**,谁都不许顶替谁 ──────────────────────────
|
|
630
|
+
// durable 腿按 `Last-Event-ID` 续读会重放同一条通告 ⇒ 消费端必须能幂等。可用的身份有两层:
|
|
631
|
+
// · `eventId` —— core 铸的稳定事件身份(uuidv7 形)。wire 今天未必带。
|
|
632
|
+
// · `eventSeq` —— **SDK 从 SSE `id:` 字段 stamp 上来的 durable 序号**(= `task_event.seq`,
|
|
633
|
+
// 见 sdk `dist/sse.js` 的 `ev.id = frame.id`;本包的规范访问口就是 `adapter/types.eventSeq`)。
|
|
634
|
+
// 它对「同一条账本行」是稳定的,重放会带同一个值。
|
|
635
|
+
// 🔴 codex 对抗复审 [medium] 采纳(2026-08-21):首版只带 `eventId`,于是 body 不带它时本臂
|
|
636
|
+
// 给消费端留的唯一去重口是 `code+ts` —— 而 `ts` 是**server 观察时刻(ms)**,同一毫秒里同码
|
|
637
|
+
// 的两条不同通告会被折成一条(真事实丢失),跨重连的同一条又可能因为观察时刻不同而重复。
|
|
638
|
+
// 明明有一个稳定序号在手却不带,是本层自己把可靠性降级了。
|
|
639
|
+
// 🔴 **绝不合并成一个键**([same-name-different-meaning-crosses-layers]):`eventId` 是引擎铸的
|
|
640
|
+
// 全局身份,`eventSeq` 是 per-task 的单调序号 —— 塞进同一个字段名会让消费端拿两种语义当一种用。
|
|
641
|
+
...(typeof ev.eventId === 'string' && ev.eventId.length > 0 ? { eventId: ev.eventId } : {}),
|
|
642
|
+
...(typeof ev.id === 'string' && ev.id.length > 0 ? { eventSeq: ev.id } : {}),
|
|
643
|
+
})));
|
|
644
|
+
}
|
|
546
645
|
/**
|
|
547
646
|
* `human_input` → 中性内部账本臂(见 `eventToSdkMessage` 顶部的 raw 预分派注释)。
|
|
548
647
|
*
|
|
@@ -111,10 +111,23 @@ export declare function pendingGateIsProvablyDifferent(prev: ActiveRunPendingGat
|
|
|
111
111
|
export interface ActiveRunBusySignal {
|
|
112
112
|
/** 占锁 run 的 id;wire 没带 ⇒ null(诚实缺席,绝不铸造)。 */
|
|
113
113
|
readonly activeTaskId: string | null;
|
|
114
|
-
/** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。
|
|
114
|
+
/** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。
|
|
115
|
+
* 🔴 [4664] 成文的可信度边界(2026-08-19,四个洞逐条在案):park 态不在 reapStale 射程 /
|
|
116
|
+
* `updated_at` 误杀窗 / 409 与 poll 口径短暂相左 / LOCAL(file)车道无周期自愈腿 ——
|
|
117
|
+
* **别把这一位的 `running` 当绝对真值**;SQL 车道死 run 的自愈上界 ≈180s(心跳 30s +
|
|
118
|
+
* REAP_RUN_STALE_SEC 120 + tick 60),LOCAL 车道**无上界**。消费方出卡前的存活对账腿见
|
|
119
|
+
* `activeRunSelfHeal.runningChoiceArm`。 */
|
|
115
120
|
readonly activeTaskStatus: string | null;
|
|
116
121
|
/** 409 body 的 pendingGate(wire 给的真路由,不自造);缺席 ⇒ null。 */
|
|
117
122
|
readonly pendingGate: ActiveRunPendingGate | null;
|
|
123
|
+
/**
|
|
124
|
+
* 409 体上的占锁 run **最近活动距今毫秒数**(Inkglow-1085 P0b,[4664]:这一格是 409 体内唯一
|
|
125
|
+
* 可消费的活性证据位)。在场 = 有限非负数;缺席 = 老 server / 坏形(两者同形,不猜)——
|
|
126
|
+
* **缺席 ≠「没有活动」**。展示判别位:端据此渲「该 run 已 N 分钟无活动」类提示;
|
|
127
|
+
* 不参与本包的分诊动作(存活对账腿的判据仍是 runs.get 读回的状态词)。
|
|
128
|
+
* ⚠️ 结构视图读(SDK 7.2.0 类型面尚无此键)—— probeCause 同款「领先锚」姿势,锚补上跟批。
|
|
129
|
+
*/
|
|
130
|
+
readonly msSinceLastActivity?: number;
|
|
118
131
|
}
|
|
119
132
|
/**
|
|
120
133
|
* 判别一个**原始 AgentEvent** 是不是 409 active-run 拒收终帧;非 busy ⇒ null。
|
|
@@ -91,10 +91,14 @@ export function activeRunBusySignal(ev) {
|
|
|
91
91
|
...(typeof cp === 'string' && cp.length > 0 ? { checkpointId: cp } : {}),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
// [4664] 活性证据位:只认**有限非负数**(NaN/Infinity/负数/串都是坏形 ⇒ 键不 stamp —— 一个
|
|
95
|
+
// 编造的「N 分钟无活动」比没有更坏)。
|
|
96
|
+
const msIdle = carrier.msSinceLastActivity;
|
|
94
97
|
return {
|
|
95
98
|
activeTaskId: cls.handle,
|
|
96
99
|
activeTaskStatus: typeof status === 'string' && status.length > 0 ? status : null,
|
|
97
100
|
pendingGate,
|
|
101
|
+
...(typeof msIdle === 'number' && Number.isFinite(msIdle) && msIdle >= 0 ? { msSinceLastActivity: msIdle } : {}),
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
* · kick 幂等(in-flight 去重),绝不 throw;
|
|
13
13
|
* · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
|
|
14
14
|
* · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
|
|
15
|
-
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL
|
|
16
|
-
*
|
|
15
|
+
* 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **失效口的由来**(#307 双扫 S25 勘误,2026-08-19)。本段此前自述「随每次
|
|
18
|
+
* `createLiveConversationClient` 构造重 kick(引擎温切重启后新构造自然重探)」——**那句话不成立**:
|
|
19
|
+
* {@link kickEngineCapsProbe} 首行就是 `capsByBase.has(baseUrl) ⇒ return`,而引擎温切
|
|
20
|
+
* (respawn / restartEngine)重启后 baseUrl 常与重启前**一模一样**,于是「新构造」被这条幂等闸
|
|
21
|
+
* 原样挡住,缓存里留的永远是**旧引擎**那一版的 caps。后果不是报错,是安静地按旧能力位走:
|
|
22
|
+
* 新引擎新增的车道被判成「没有」(藏功能),旧引擎有而新引擎撤掉的车道被判成「有」(走死路)。
|
|
23
|
+
* 此前除测试钩 {@link __resetEngineCapsCacheForTests} 外**没有任何生产失效路径**。
|
|
24
|
+
* 修 = 显式失效口 {@link invalidateEngineCaps},由知道「引擎换人了」的那一层(壳的 respawn /
|
|
25
|
+
* restartEngine)在重启后调用 —— 缓存自己无从分辨「同一个 baseUrl 后面还是不是同一个引擎」,
|
|
26
|
+
* 猜(TTL / 每次构造清)只会把一个确定事实换成一个定时器。
|
|
17
27
|
*/
|
|
18
28
|
/** 构造期 kick(async 幂等);probe = client.capabilities 薄闭包。 */
|
|
19
29
|
export declare function kickEngineCapsProbe(baseUrl: string, probe: () => Promise<unknown>): void;
|
|
@@ -24,7 +34,39 @@ export declare function kickEngineCapsProbe(baseUrl: string, probe: () => Promis
|
|
|
24
34
|
* 绝不 reject(探测失败=位维持未判,调用方按缺席降级)。
|
|
25
35
|
*/
|
|
26
36
|
export declare function engineCapsSettled(baseUrl: string | undefined): Promise<void>;
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* 能力位的**可分辨读数**(#318 件③,cli [4752] 自领缺口2)——{@link engineCapTrue} 的三态化底座。
|
|
39
|
+
*
|
|
40
|
+
* 病:`engineCapTrue` 只回 `true`/`false`,于是 `false` 同时承载**四件互不相同**的事 ——
|
|
41
|
+
* 「引擎明说没有」「还没探」「探测在飞」「探测失败」。对**放行判据**而言这个塌缩是正确且刻意的
|
|
42
|
+
* (缺席一律 fail-closed 不渲 affordance,见 engineCapTrue 的头注),但对**自检/诊断面**(doctor)
|
|
43
|
+
* 它是致命的:doctor 要报的正是「这台引擎到底说了什么」,把「不知道」印成「没有」就是谎报。
|
|
44
|
+
* 这也是 doctor 此前无法复用本共享缓存、只能自己再探一遍的直接原因。
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **闭集,且每个成员各自可行动**(A1 禁哨兵值双义):
|
|
47
|
+
* · `true` / `false` —— caps **已落地**且该键字面为该布尔值 = **引擎明说**;
|
|
48
|
+
* · `unprobed` —— 该 base 此刻没有已落地的 caps(没 kick / 在飞 / 探测失败 / 刚被
|
|
49
|
+
* {@link invalidateEngineCaps} 作废)。🔴 四种成因**故意合并**:它们对调用方是同一个动作 ——
|
|
50
|
+
* `await` 一次 {@link engineCapsSettled} 再读,或按「不知道」呈现。分开需要引擎没给的信息;
|
|
51
|
+
* · `absent` —— caps 已落地,但**没有这个键**:引擎比本包旧(还没这条车道)或比本包新(改名了);
|
|
52
|
+
* · `non_boolean` —— caps 已落地、键也在,但值不是布尔。这一档**不并进 `absent`**:那会是假话
|
|
53
|
+
* (键在),而它指向的是真问题(引擎申报了一个本口读不动的形 —— 比如把 caps 位写成了字符串),
|
|
54
|
+
* 诊断面该看见它。
|
|
55
|
+
*
|
|
56
|
+
* 🔴 **本读口不改变任何放行语义**:{@link engineCapTrue} 逐字等价于 `engineCapState(...) === 'true'`
|
|
57
|
+
* (下面就是这么实现的,单源)。放行面**继续**用 `engineCapTrue` —— 拿三态去开放行分支,等于把
|
|
58
|
+
* fail-closed 改成「按成因区别对待」,那是另一件事,不在本口的授权内。
|
|
59
|
+
*/
|
|
60
|
+
export type EngineCapState = 'true' | 'false' | 'unprobed' | 'absent' | 'non_boolean';
|
|
61
|
+
/**
|
|
62
|
+
* 同步读口(三态化):该 base 的 caps 里 key 的**可分辨**读数。语义逐条见 {@link EngineCapState}。
|
|
63
|
+
*
|
|
64
|
+
* `baseUrl` 缺席 ⇒ `unprobed`(没有 base 就没有任何一次探测,这不是「引擎说没有」)。
|
|
65
|
+
*/
|
|
66
|
+
export declare function engineCapState(baseUrl: string | undefined, key: string): EngineCapState;
|
|
67
|
+
/** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。
|
|
68
|
+
* 🔴 **语义一字不变**(#318 件③ 三态化后改为经 {@link engineCapState} 单源实现):放行面读的就是
|
|
69
|
+
* 这一口,四种「不知道」继续一律折成 `false` = fail-closed。要分辨成因请读 {@link engineCapState}。 */
|
|
28
70
|
export declare function engineCapTrue(baseUrl: string | undefined, key: string): boolean;
|
|
29
71
|
/**
|
|
30
72
|
* 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
|
|
@@ -35,5 +77,41 @@ export declare function engineCapTrue(baseUrl: string | undefined, key: string):
|
|
|
35
77
|
* 引擎能力位缺席时任何「假定它有」的分支都是对用户/模型的谎报。
|
|
36
78
|
*/
|
|
37
79
|
export declare function engineCapString(baseUrl: string | undefined, key: string): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* **生产失效口**(#307 双扫 S25,client-core 0.37.0):把该 base 的探测结果作废,
|
|
82
|
+
* 使**下一次** {@link kickEngineCapsProbe} 真正重探(而不是被幂等闸原样挡回)。
|
|
83
|
+
*
|
|
84
|
+
* 消费方 = **知道「这个 baseUrl 后面换了一个引擎进程」的那一层**,目前唯一一处是壳的引擎温切:
|
|
85
|
+
* cli `respawn` / `restartEngine` 成功后、重新构造 `createLiveConversationClient` **之前**调用。
|
|
86
|
+
* 库这一层看到的只有一个字符串 base,分辨不出对面是不是同一个进程,所以失效必须由上面显式下达 ——
|
|
87
|
+
* 见模块头注:靠 TTL 或「每次构造清」去猜,是把一个确定事实换成一个定时器。
|
|
88
|
+
*
|
|
89
|
+
* 语义与边界:
|
|
90
|
+
* · **推进代际**({@link genByBase}) + 清 `capsByBase`(已判结果) + 清 `inFlight`(在途去重位)。
|
|
91
|
+
* · **不 abort** 在途探测(没有可 abort 的把手,probe 是调用方给的薄闭包),改用代际让它
|
|
92
|
+
* **安静退场**:被顶掉的旧 run 落地后既不写缓存、也不归还任何位。所以本口在**探测在途时
|
|
93
|
+
* 调用是安全的** —— 旧引擎那次响应绝不会覆盖新引擎的能力位(2026-08-19 对抗复审 [high])。
|
|
94
|
+
* · `settleByBase` **不清**:{@link engineCapsSettled} 的语义是「等**当前这一次**探测落地」,
|
|
95
|
+
* 把在途 promise 抽走会让正在 await 的调用方立即拿到 resolve(假「已落地」)。旧 run 的
|
|
96
|
+
* settle 位在被新 run 顶掉后由代际认领保护,旧 run 的 `finally` 不会误删。
|
|
97
|
+
* · 空串 ⇒ no-op。**从没探过也从没失效过的 base ⇒ 真 no-op**(不留代际条目):那种 base 上
|
|
98
|
+
* 既无判定也无在途 run,没有任何陈旧写入可挡,留条目只会让任意串撑大表。
|
|
99
|
+
* · 绝不 throw(与本模块其余口同款:失效口在错误路径上响 = 把一个清理动作变成新的故障源)。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 **推荐用两参形 `invalidateEngineCaps(baseUrl, probe)`**(对抗复审第三轮 [medium] 采纳,
|
|
102
|
+
* 2026-08-19)。单参形与「下一次 kick」之间有一个**真窗**:失效之后、新探测注册之前,如果旧代际
|
|
103
|
+
* 探测正好在这一拍落地,`settleByBase` 里已经没有更新代际的条目 ⇒ {@link engineCapsSettled} 把
|
|
104
|
+
* 等待者放走,而缓存刚被清空 ⇒ 等待者把**当代引擎的能力位读成缺席**。窗口只在调用方于失效与 kick
|
|
105
|
+
* 之间 `await` 了什么时才张开(同步块里 JS 单线程,旧探测的续体根本插不进来),但引擎温切本身就是
|
|
106
|
+
* 异步流程,所以它是可达的。
|
|
107
|
+
* 两参形把「推进代际」与「注册替代探测」放进**同一个同步块** ⇒ 窗口按构造不存在,等待者会被接力
|
|
108
|
+
* 到新探测上(见 {@link engineCapsSettled} 的跨代际接力)。壳的 respawn/restartEngine 应当用它。
|
|
109
|
+
* 单参形保留给「只想丢掉缓存、这一刻没有替代探测」的调用方 —— 那种情形下等待者读到**未判**
|
|
110
|
+
* 是诚实结局(判据永远是缓存位),不是缺陷:硬等一个可能永远不会来的 kick 才是。
|
|
111
|
+
*
|
|
112
|
+
* @param probe 可选的**替代探测**(与新引擎同一拍注册)。给了就等价于「失效 + 立刻 kick」,
|
|
113
|
+
* 且中间没有任何可插入点。
|
|
114
|
+
*/
|
|
115
|
+
export declare function invalidateEngineCaps(baseUrl: string | undefined, probe?: () => Promise<unknown>): void;
|
|
38
116
|
/** 测试钩子。 */
|
|
39
117
|
export declare function __resetEngineCapsCacheForTests(): void;
|