@sema-agent/client-core 0.52.0 → 0.54.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 +142 -1
- package/README.md +2 -1
- package/dist/adapt/arms.js +31 -3
- package/dist/adapter/activeRunSelfHeal.d.ts +99 -2
- package/dist/adapter/activeRunSelfHeal.js +381 -104
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/peerFrames.d.ts +117 -0
- package/dist/peerFrames.js +369 -0
- package/docs/INTEGRATION-CLIENTS.md +257 -9
- package/package.json +1 -1
|
@@ -138,6 +138,9 @@ export function selfHealSubmissionDisposition(outcome) {
|
|
|
138
138
|
// reopen-failed 那一臂)⇒ 用户手上确有把手。
|
|
139
139
|
return 'held-for-decision';
|
|
140
140
|
case 'running-cancelled':
|
|
141
|
+
case 'stale-park-cancelled':
|
|
142
|
+
// L-93:与 running-cancelled 同一条重发腿 —— 前提也同一个(引擎**确认**那条 run 不再占着会话,
|
|
143
|
+
// 判据 = CLAIM_RELEASED_STATES;确认不到的两形落 timeout/failed ⇒ default 的 not-delivered)。
|
|
141
144
|
case 'ask-decided-without-card':
|
|
142
145
|
return 'resending';
|
|
143
146
|
case 'ask-decided-release-unknown':
|
|
@@ -230,10 +233,32 @@ function runningChoiceDeclineKey(sessionId, taskId) {
|
|
|
230
233
|
// NUL 分隔:两段都是外来串,可打印分隔符在「sessionId 里恰有它」时会串键。
|
|
231
234
|
return `${sessionId ?? ''}\u0000${taskId}`;
|
|
232
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* L-93 两选卡(陈旧 park)的登记键 —— 与三选卡**同一张表、不同键**。
|
|
238
|
+
* 🔴 为什么不共用同一个键:两张卡问的是不同的问题(「这条**在跑**的 run 怎么办」vs「这条 park 住、
|
|
239
|
+
* 而审批已经**不在引擎上**的 run 怎么办」),用户对其中一张说过「什么都不做」不构成对另一张的表态
|
|
240
|
+
* —— 共键会让一次旧表态把一张他从没见过的卡压掉。共用一张表只为共用那条 FIFO 上界与同一个清口。
|
|
241
|
+
*/
|
|
242
|
+
function staleParkDeclineKey(sessionId, taskId) {
|
|
243
|
+
return `${runningChoiceDeclineKey(sessionId, taskId)}\u0000stale-park`;
|
|
244
|
+
}
|
|
245
|
+
/** 登记一条「用户选了什么都不做」+ FIFO 有界驱逐(两张卡共用 —— 各写一份必漂,而漂的后果是
|
|
246
|
+
* 其中一张卡的登记表悄悄变成无界)。驱逐代价 = 那条 run 的卡重弹一次(= 修前行为,方向安全)。 */
|
|
247
|
+
function rememberDeclinedOffer(key) {
|
|
248
|
+
runningChoiceDeclined.add(key);
|
|
249
|
+
if (runningChoiceDeclined.size > RUNNING_CHOICE_LEDGER_CAP) {
|
|
250
|
+
const oldest = runningChoiceDeclined.values().next().value;
|
|
251
|
+
if (oldest !== undefined)
|
|
252
|
+
runningChoiceDeclined.delete(oldest);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
233
255
|
/** 清掉某条 run 的「Do nothing」登记 —— 宿主「重新打开操作菜单」入口(下一次 409 重新整卡呈现)。
|
|
234
|
-
* `sessionId` 与当时喂给 {@link ActiveRunSelfHealDeps.sessionId} 的值同源(缺席 = 默认键)。
|
|
256
|
+
* `sessionId` 与当时喂给 {@link ActiveRunSelfHealDeps.sessionId} 的值同源(缺席 = 默认键)。
|
|
257
|
+
* 🔴 三选卡与两选卡(L-93)的登记**一起清**:这个入口的语义是「让我重新对这条 run 表态」,
|
|
258
|
+
* 只清其中一张会让另一张在宿主已经答应「重新问」之后仍旧被压掉(paired-mechanisms 同族)。 */
|
|
235
259
|
export function clearRunningChoiceOffer(taskId, sessionId) {
|
|
236
260
|
runningChoiceDeclined.delete(runningChoiceDeclineKey(sessionId, taskId));
|
|
261
|
+
runningChoiceDeclined.delete(staleParkDeclineKey(sessionId, taskId));
|
|
237
262
|
}
|
|
238
263
|
/** 测试钩:清空整张登记表(套件各组之间隔离用)。 */
|
|
239
264
|
export function __resetRunningChoiceLedgerForTests() {
|
|
@@ -363,6 +388,42 @@ export async function waitForClaimRelease(taskId, deps) {
|
|
|
363
388
|
}
|
|
364
389
|
}
|
|
365
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* 有界问一次「引擎侧这个会话名下还有几行**属主**待决审批」(`deps.listOwnedPendingApprovals`)。
|
|
393
|
+
*
|
|
394
|
+
* 返回 `number` = 引擎**答了**这个数;返回 `null` = **没答上来**(抛错 / 窗尽 / 调用方中止)——
|
|
395
|
+
* 两者是完全不同的事实,调用方各自决定怎么用(本文件两个调用点都把 `null` 当「不知道」保守处置)。
|
|
396
|
+
*
|
|
397
|
+
* 🔴 有界且可回收(#244 F1 复审 [high]+[medium]×2 的成文形):probe 恒不落地不许把整棵分诊树挂死;
|
|
398
|
+
* 合流 signal **真交到 probe 上**(掐的是底层请求,不是只掐调用方的 await);竞速败方在 finally 里
|
|
399
|
+
* 统一 abort(快成功时那只 4s 普通计时器不清,`-p` 车道进程会被它多拖活一窗)。
|
|
400
|
+
* 🔴 单源:两个调用点(假死锁防御 / L-93 开枪前复证)共用这一只 —— 各写一份必漂,而这两处漂的
|
|
401
|
+
* 后果分别是「Esc 之后接着弹卡」与「拿一次没答上来的读当成前提成立」。
|
|
402
|
+
*/
|
|
403
|
+
async function readOwnedPendingCount(read, deps) {
|
|
404
|
+
const recheckCtl = new AbortController();
|
|
405
|
+
const onCallerAbort = () => {
|
|
406
|
+
recheckCtl.abort(deps?.signal?.reason);
|
|
407
|
+
};
|
|
408
|
+
deps?.signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
409
|
+
try {
|
|
410
|
+
const count = await Promise.race([
|
|
411
|
+
read({ signal: recheckCtl.signal }),
|
|
412
|
+
claimPollSleep(OWNED_PENDING_RECHECK_TIMEOUT_MS, recheckCtl.signal).then(() => null),
|
|
413
|
+
]);
|
|
414
|
+
// 已中止(进门前/竞速期间)一律不采信这个数 —— Esc 之后拿它驱动任何动作都是「用户说停还在动」。
|
|
415
|
+
if (deps?.signal?.aborted === true)
|
|
416
|
+
return null;
|
|
417
|
+
return typeof count === 'number' ? count : null;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return null; // 读失败 = 「不知道」,不是「零」
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
deps?.signal?.removeEventListener('abort', onCallerAbort);
|
|
424
|
+
recheckCtl.abort(new Error('owned-pending recheck settled'));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
366
427
|
/**
|
|
367
428
|
* 自愈一次:定门(kind 优先 → status 表回退)→ 按门类重开对应的卡 → 回报结局。**绝不抛**
|
|
368
429
|
* (turn 收尾路径)。kind 在场时不再回查 runs.get —— wire 已指名门身份,省一次往返。
|
|
@@ -390,26 +451,10 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
|
|
|
390
451
|
// · 竞速败方必须收口:快成功时 4s sleep 的普通计时器不清,-p 车道进程会被它多拖活一窗
|
|
391
452
|
// (finally 统一 abort ⇒ abortableSleep 清 timer、在飞 probe 被掐);
|
|
392
453
|
// · 已中止(进门前/竞速期间)一律不采信 count —— Esc 之后接着放行重呈 = 用户说停还弹卡。
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
deps.signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
398
|
-
try {
|
|
399
|
-
const count = await Promise.race([
|
|
400
|
-
deps.listOwnedPendingApprovals({ signal: recheckCtl.signal }),
|
|
401
|
-
claimPollSleep(OWNED_PENDING_RECHECK_TIMEOUT_MS, recheckCtl.signal).then(() => null),
|
|
402
|
-
]);
|
|
403
|
-
if (count === 0 && !callerAborted())
|
|
404
|
-
pending = false;
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
/* 复核失败分不出真卡与幽灵 —— 维持 stand down,下一条消息再走一遍本腿 */
|
|
408
|
-
}
|
|
409
|
-
finally {
|
|
410
|
-
deps.signal?.removeEventListener('abort', onCallerAbort);
|
|
411
|
-
recheckCtl.abort(new Error('owned-pending recheck settled'));
|
|
412
|
-
}
|
|
454
|
+
// 这四条的实现在共享叶 {@link readOwnedPendingCount}(L-93 开枪前复证走同一只)。
|
|
455
|
+
const count = await readOwnedPendingCount(deps.listOwnedPendingApprovals, deps);
|
|
456
|
+
if (count === 0 && !callerAborted())
|
|
457
|
+
pending = false;
|
|
413
458
|
}
|
|
414
459
|
if (pending)
|
|
415
460
|
return { kind: 'decision-pending', taskId: signal.activeTaskId };
|
|
@@ -520,8 +565,243 @@ async function askParkArm(taskId, signal, runs, deps) {
|
|
|
520
565
|
}
|
|
521
566
|
return { kind: 'ask-decided-release-unknown', taskId, lastStatus: release.lastStatus };
|
|
522
567
|
}
|
|
568
|
+
// L-93:重开链在有界窗内**读面答了**、而属主待决行**恰 0**(引擎重启丢行 / 审批被 reap),run
|
|
569
|
+
// 却仍 park ⇒ 卡不是「呈不出来」而是「没有东西可呈」,走陈旧 park 臂(见 {@link staleParkArm})。
|
|
570
|
+
// 🔴 位缺席/含糊值一律不进(只认严格 `true`):这是本层允许 cancel 一条 park run 的唯一前提,
|
|
571
|
+
// 松掉它就是把退役 cancel 臂的循环病根放回来。`decidedWithoutCard` 已在上面收口,此处再写一次
|
|
572
|
+
// 是把「两位同时在场时谁优先」钉在判据里,不靠上面那个 return 的顺序当默契。
|
|
573
|
+
if (verdict.reopened === false && verdict.decidedWithoutCard !== true && verdict.pendingRowGone === true) {
|
|
574
|
+
return staleParkArm(taskId, signal, runs, deps);
|
|
575
|
+
}
|
|
523
576
|
return { kind: 'ask-reopen-failed', taskId, decidePath: signal.pendingGate?.decidePath ?? null };
|
|
524
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* 有界打一发 `runs.get` 读真态(lease 带截止 + 合流调用方中止;404 单列,其余错误一律「没答上来」)。
|
|
580
|
+
* 🔴 单源:running 存活对账腿、陈旧 park 臂的**卡前**与**卡后**三处共用这一只 —— 各写一份必漂,
|
|
581
|
+
* 而漂的后果是其中一处忘了 404 / 本发截止 / 奇形记录三种缺席的处置。
|
|
582
|
+
* 🔴 接收者必保:写成 `runs.get(...)` 方法形(把方法摘下来裸调 = 真 SDK 上当场 TypeError)。
|
|
583
|
+
*/
|
|
584
|
+
async function probeRunStatusOnce(taskId, durable, durableGet, deps) {
|
|
585
|
+
let status = null;
|
|
586
|
+
let ghost = false;
|
|
587
|
+
const lease = claimProbeLease(deps?.signal, RUNNING_LIVENESS_RECHECK_TIMEOUT_MS);
|
|
588
|
+
try {
|
|
589
|
+
// 保接收者(`durableGet.call(durable, …)`):摘下来裸调在真 SDK 上当场 TypeError。
|
|
590
|
+
status = readStatus(await durableGet.call(durable, taskId, { signal: lease.signal, ...sessionOpts(deps) }));
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
if (e?.status === 404)
|
|
594
|
+
ghost = true;
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
lease.release();
|
|
598
|
+
}
|
|
599
|
+
return { status, ghost };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* L-93 陈旧 park 臂(0.53.0):run 仍停在 park 态,而它在等的那张审批**已经不在引擎上**
|
|
603
|
+
* (宿主经 {@link ReopenCardVerdict} 的 `pendingRowGone` 证出「读面答了 + 零属主待决行」)。
|
|
604
|
+
*
|
|
605
|
+
* ── 为什么这一形可以给 cancel,而退役的那条臂不行 ─────────────────────────────────────────────
|
|
606
|
+
* 退役 cancel 臂的循环病根 = 「**待决项仍在**」时 cancel 的语义是替用户 deny 掉它 ⇒ 模型重试 ⇒
|
|
607
|
+
* 新 run 停在同一道门上 ⇒ 再 park。本臂的前提恰恰是那个待决项**已经不存在**:没有任何东西可被
|
|
608
|
+
* 「否掉」,那条循环在结构上不成立。所以边界写在**判定**上,不写在文案上 ——
|
|
609
|
+
* ① `pendingRowGone` 缺席/含糊值 ⇒ 本臂根本进不来(调用点只认严格 `true`);
|
|
610
|
+
* ② 进来之后仍要用 `runs.get` **一手复核**(宿主的一句话不足以驱动破坏性动作);
|
|
611
|
+
* ③ 复核读回的必须是**审批 park 词**({@link ASK_PARK_STATES});plan park(`needs_review`)交给
|
|
612
|
+
* plan 重开臂 —— 对它 cancel = 替用户丢掉整个 plan,是另一条禁区;
|
|
613
|
+
* ④ 两选卡上用户必须**逐字**选 `cancel` 才动那一枪(Esc/空答/读不出一律 wait)。
|
|
614
|
+
* 任何一条不成立 ⇒ 退回既有 `ask-reopen-failed`(老宿主与不确定情形逐字节零行为差)。
|
|
615
|
+
*/
|
|
616
|
+
async function staleParkArm(taskId, busy, runs, deps) {
|
|
617
|
+
/** 本臂的**保守收口**:与修前逐字同一行(卡呈不出来/读不出真态/不确定 ⇒ 零动作 + 如实说)。 */
|
|
618
|
+
const reopenFailed = {
|
|
619
|
+
kind: 'ask-reopen-failed',
|
|
620
|
+
taskId,
|
|
621
|
+
decidePath: busy.pendingGate?.decidePath ?? null,
|
|
622
|
+
};
|
|
623
|
+
// 读口缺席 ⇒ 一手复核做不成 ⇒ 不猜(宿主的一句话不驱动破坏性动作)。
|
|
624
|
+
// 🔴 **只取来判在场,绝不裸调**(runningChoiceArm R7 同注):真 SDK 的 `RunsResource` 方法体走
|
|
625
|
+
// `this.t.request(...)`,摘下来单独调当场 TypeError —— 下面的真调用一律写成 `runs.get(...)` 方法形,
|
|
626
|
+
// 交给 cancel 腿的那一份也用它保接收者的包装。
|
|
627
|
+
if (runs === undefined || typeof runs.get !== 'function')
|
|
628
|
+
return reopenFailed;
|
|
629
|
+
const durableGet = runs.get;
|
|
630
|
+
// 🔴 经函数读 aborted(waitForClaimRelease.isAborted 同注):它在 await 两侧会变,直接读两次会被
|
|
631
|
+
// tsc 控制流分析把第二次窄成恒假比较(TS2367)。
|
|
632
|
+
const callerAborted = () => deps?.signal?.aborted === true;
|
|
633
|
+
// 进门前已中止 ⇒ 一发出站都不打(Esc 之后还去问引擎 = 用户说停还在动)。
|
|
634
|
+
if (callerAborted())
|
|
635
|
+
return reopenFailed;
|
|
636
|
+
// ── 一手复核(与 running 存活对账腿同款 lease/abort 处理)────────────────────────────────────
|
|
637
|
+
const first = await probeRunStatusOnce(taskId, runs, durableGet, deps);
|
|
638
|
+
// 幽灵 claim:引擎一边说这个会话被这条 run 占着、一边查无此 run(与 running-not-found 同律 ——
|
|
639
|
+
// 404 是「不存在」∪「不是你的」的不可分辨并集,所以只说已证的事实,绝不盲发那条消息)。
|
|
640
|
+
if (first.ghost)
|
|
641
|
+
return { kind: 'ask-run-not-found', taskId };
|
|
642
|
+
// 复核期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记/cancel)都不做。
|
|
643
|
+
if (callerAborted())
|
|
644
|
+
return reopenFailed;
|
|
645
|
+
// 读不出(网络失败 / 本发截止 / 奇形记录)⇒ 「读不到」≠「行不在」⇒ 保守收口,不猜。
|
|
646
|
+
const fresh = first.status;
|
|
647
|
+
if (fresh === null)
|
|
648
|
+
return reopenFailed;
|
|
649
|
+
// 真态是「在跑」⇒ 交给既有三选卡臂(statusFromWire=false:这一发就是新鲜读数,不打第二发)。
|
|
650
|
+
if (RUNNING_STATES.includes(fresh))
|
|
651
|
+
return runningChoiceArm(taskId, fresh, busy, false, runs, deps);
|
|
652
|
+
// 🔴 plan park ⇒ 交给 plan 重开臂:对它 cancel 是替用户把整个 plan 丢掉(禁区),而 plan 卡
|
|
653
|
+
// 本身是能重开的 —— 递一条更好的路,而不是递一把更危险的把手。
|
|
654
|
+
if (PLAN_REVIEW_STATES.includes(fresh))
|
|
655
|
+
return planReviewArm(taskId, busy, deps);
|
|
656
|
+
// 表外的状态词(含今天还不存在的)⇒ 不认得就不给把手,保守收口。
|
|
657
|
+
if (!ASK_PARK_STATES.includes(fresh))
|
|
658
|
+
return reopenFailed;
|
|
659
|
+
// ── 两选卡:停掉它 / 什么都不做 ──────────────────────────────────────────────────────────────
|
|
660
|
+
const offer = deps?.offerStaleParkChoice;
|
|
661
|
+
if (typeof offer !== 'function')
|
|
662
|
+
return reopenFailed; // 老宿主:整臂不走,行为逐字节不变
|
|
663
|
+
// 供给位 = ①cancel 动词在场、②有能确认释放的 `get`(上面的一手复核已证在场)、③有**开枪前
|
|
664
|
+
// 再证一次前提**的属主待决行读口(`deps.listOwnedPendingApprovals`)。
|
|
665
|
+
// 🔴 第③条是 异源对抗复审 R1 [high] 的采纳(见下面开枪前那一段):`pendingRowGone` 是**读那一刻**的
|
|
666
|
+
// 事实,而这张卡是人在看、可以看任意久;没有复证口就没法在开枪前把那个前提再证一次,
|
|
667
|
+
// 于是「停掉它」这条路在本层就**兑现不了它承诺的安全性** ⇒ 按供给位纪律整卡不呈(退回旧结局),
|
|
668
|
+
// 而不是先呈卡再在开枪时才发现证不了。宿主要拿到这张卡,就得把这个读口一起注入(见 §16)。
|
|
669
|
+
// 两条路只剩「什么都不做」时整卡不呈 —— 只有一个空动作的卡是纯噪音(与三选卡 G8②b 同律)。
|
|
670
|
+
const recheckOwnedPending = deps?.listOwnedPendingApprovals;
|
|
671
|
+
const canCancel = typeof runs.cancel === 'function' && typeof recheckOwnedPending === 'function';
|
|
672
|
+
// 第二段是给 tsc 的收窄(`canCancel` 是布尔,narrow 不了 `recheckOwnedPending` 这只可选函数),
|
|
673
|
+
// 判据本身只有 `canCancel` 一条。
|
|
674
|
+
if (!canCancel || typeof recheckOwnedPending !== 'function')
|
|
675
|
+
return reopenFailed;
|
|
676
|
+
const declineKey = staleParkDeclineKey(deps?.sessionId, taskId);
|
|
677
|
+
// 同 (sessionKey,taskId) 已呈过且用户选了「什么都不做」⇒ 不再整卡重弹,给判别位让端渲一行。
|
|
678
|
+
if (runningChoiceDeclined.has(declineKey)) {
|
|
679
|
+
return { kind: 'stale-park-wait', taskId, status: fresh, alreadyOffered: true };
|
|
680
|
+
}
|
|
681
|
+
let choice = null;
|
|
682
|
+
try {
|
|
683
|
+
// 🔴 fail-closed:**只有逐字 `'cancel'`** 才武装那一枪;Esc/空答/表外词一律收口成 wait。
|
|
684
|
+
choice = (await offer({ taskId, status: fresh, canCancel })) === 'cancel' ? 'cancel' : 'wait';
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
choice = null; // 呈卡腿出意外 = 用户没看到卡、更没做决定 ⇒ 退回旧结局(绝不掉进破坏性分支)
|
|
688
|
+
}
|
|
689
|
+
if (choice === null)
|
|
690
|
+
return reopenFailed;
|
|
691
|
+
if (choice === 'wait') {
|
|
692
|
+
// 呈卡腿抛错那次**不**登记(用户没看到卡,压掉后续等于把没呈过的卡当已呈)—— 见上面 catch。
|
|
693
|
+
rememberDeclinedOffer(declineKey);
|
|
694
|
+
return { kind: 'stale-park-wait', taskId, status: fresh };
|
|
695
|
+
}
|
|
696
|
+
// 用户重新表了态 ⇒ 清登记(与三选卡 steer/cancel 同律)。
|
|
697
|
+
runningChoiceDeclined.delete(declineKey);
|
|
698
|
+
// ── 🔴 卡后**复证 run 真态**(异源对抗复审 R3 [high] 采纳)────────────────────────────────────────────
|
|
699
|
+
// 上面那一发 `runs.get` 打在**呈卡之前**,而卡是人在看、可以停留任意久 —— 这中间那条 run 完全
|
|
700
|
+
// 可能已经**恢复运行**(审批在别处被决断 / 会话重新附着 / 引擎自愈)。沿用旧卡上那句 cancel 去停
|
|
701
|
+
// 一条**正在干活**的 run,等于拿「清理一个死结」的授权丢掉正在进行的工作,而用户按下去时屏幕上
|
|
702
|
+
// 写的前提早就不成立了。所以先复证状态仍是审批 park 词,再谈开枪:
|
|
703
|
+
// · 仍 park ⇒ 继续;· 404 ⇒ 幽灵 claim 如实说;· 变成别的状态 / 读不出 / 已中止 ⇒ 一律
|
|
704
|
+
// fail-closed 退回旧结局(下一条消息重新走整棵分诊树,拿到与那时真态相符的那张卡)。
|
|
705
|
+
if (callerAborted())
|
|
706
|
+
return reopenFailed;
|
|
707
|
+
const afterCard = await probeRunStatusOnce(taskId, runs, durableGet, deps);
|
|
708
|
+
if (afterCard.ghost)
|
|
709
|
+
return { kind: 'ask-run-not-found', taskId };
|
|
710
|
+
if (callerAborted())
|
|
711
|
+
return reopenFailed;
|
|
712
|
+
if (afterCard.status === null || !ASK_PARK_STATES.includes(afterCard.status))
|
|
713
|
+
return reopenFailed;
|
|
714
|
+
// ── 🔴 开枪前**再证一次前提**(异源对抗复审 R1 [high] 采纳)──────────────────────────────────────────
|
|
715
|
+
// `pendingRowGone` 是宿主**读那一刻**的事实,而这张卡是人在看、可以看任意久;这中间待决行完全
|
|
716
|
+
// 可能重新出现(store 恢复 / 会话重新附着),宿主也可能把「读失败 / 找不到能用的那一行」误报成
|
|
717
|
+
// gone。而上面那一发 `runs.get` 只证得了「它仍 parked」—— **审批还在**与**审批已丢**两种情形读回
|
|
718
|
+
// 的都是 `suspended`,那一发从来就证不了这个前提。代价不对称:cancel 一条**其实还能被决断**的
|
|
719
|
+
// run = 用户在一句假前提(「那张审批已经不在了」)下把整轮工作丢掉,而他以为自己只是在清理一个
|
|
720
|
+
// 死结。所以在**离 runs.cancel 最近的那一刻**再问一次属主待决行数:
|
|
721
|
+
// · 恰 0(引擎答了)⇒ 前提仍成立,开枪;
|
|
722
|
+
// · 非 0 / 抛错 / 窗尽 / 调用方中止 / 读不出 ⇒ **一律不开枪**(fail-closed),退回旧结局
|
|
723
|
+
// —— 「不知道」和「行又回来了」都不构成销毁一条 run 的授权。
|
|
724
|
+
// 🔴 **在册边界(P-44,异源对抗复审 R2 [high] 如实登记)**:这是「查了再做」,**不是原子条件取消**。
|
|
725
|
+
// 复证与那一枪之间仍有毫秒级窗口,行恰在此间恢复的话那一枪照样落下去。客户端关不死它 ——
|
|
726
|
+
// 真正的关法是引擎侧的**条件取消**(带审批快照版本 / checkpoint 标识,条件变了回 409 且不取消),
|
|
727
|
+
// 那是 wire 能力不是壳能自造的语义。这里能做的是把窗口从「人看卡的任意长时间」压到最小,
|
|
728
|
+
// 并把剩余风险登记在册(docs/INTEGRATION-CLIENTS.md §7b P-44),不假装它不存在。
|
|
729
|
+
const stillGone = await readOwnedPendingCount(recheckOwnedPending, deps);
|
|
730
|
+
if (stillGone !== 0)
|
|
731
|
+
return reopenFailed;
|
|
732
|
+
// 🔴 **复用**同一条 cancel + 有界释放确认腿(不复制):失败分类与释放判据必须与三选卡同源。
|
|
733
|
+
const verdict = await cancelAndConfirmRelease(taskId, runs, durableGet, deps);
|
|
734
|
+
if (verdict.outcome === 'released')
|
|
735
|
+
return { kind: 'stale-park-cancelled', taskId };
|
|
736
|
+
if (verdict.outcome === 'failed') {
|
|
737
|
+
return { kind: 'stale-park-cancel-failed', taskId, detail: verdict.detail, delivery: verdict.delivery };
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
kind: 'stale-park-cancel-timeout',
|
|
741
|
+
taskId,
|
|
742
|
+
waitedMs: verdict.waitedMs,
|
|
743
|
+
aborted: verdict.aborted,
|
|
744
|
+
confirmedHeld: verdict.confirmedHeld,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* cancel 恰一枪 + 有界等那条 run 交出会话。
|
|
749
|
+
*
|
|
750
|
+
* 🔴 cancel 的失败分两类(二次评审 R4 [high]),与 steer 同一把尺:
|
|
751
|
+
* · `rejected`(4xx)—— 服务端答了且明确拒 ⇒ 那一枪确定没生效,立即如实说,不必再等确认腿;
|
|
752
|
+
* · `unknown`(5xx / 连接在回执路上断了)—— 请求**可能已经被受理并在异步执行**。此时直接收口成
|
|
753
|
+
* 「取消失败,那条 run 还占着会话」是对用户下一个证不出的断言 —— 而这条腿手上恰恰有一个能
|
|
754
|
+
* 回答这个问题的读口(`get`)。所以照走有界确认腿:真交出来了就算取消成了,确认不到才如实说
|
|
755
|
+
* 「没能确认」。判据锚在**决定量**(会话有没有被交出来)上,不锚它的一个前置条件(那一枪的
|
|
756
|
+
* 回执长什么样)。
|
|
757
|
+
* 🔴 接收者必保:真 SDK 的 `RunsResource` 方法体走 `this.t.request(...)`,把方法摘下来裸调当场
|
|
758
|
+
* TypeError —— 所以 cancel 写成方法形 `durable.cancel?.(…)`,交给等待器的 `get` 用保接收者的包装。
|
|
759
|
+
*/
|
|
760
|
+
async function cancelAndConfirmRelease(taskId, durable, durableGet, deps) {
|
|
761
|
+
const boundGet = (id, opts) => durableGet.call(durable, id, opts);
|
|
762
|
+
let cancelFailure = null;
|
|
763
|
+
const cancelWaitMs = deps?.cancelReleaseWaitMs ?? CANCEL_RELEASE_WAIT_MS;
|
|
764
|
+
const cancelStartedAt = Date.now();
|
|
765
|
+
const cancelLease = claimProbeLease(deps?.signal, cancelWaitMs);
|
|
766
|
+
try {
|
|
767
|
+
// 🔴 这一枪也必须**带中止口与截止**(二次评审 R5 [high]):`cancel` 的 promise 不落地,后面那条
|
|
768
|
+
// 有界确认腿就根本起不来 —— 用户按了 Esc 既停不掉等待也停不掉出站请求,整条腿零结局。有界性
|
|
769
|
+
// 的执行手段只有一个:把 signal 真交到请求上。
|
|
770
|
+
await durable.cancel?.(taskId, { signal: cancelLease.signal, ...sessionOpts(deps) });
|
|
771
|
+
}
|
|
772
|
+
catch (e) {
|
|
773
|
+
// 用户中止 ⇒ 当拍收口(绝不接着跑一个满窗的确认腿:「UI 说停了、后台还在问」正是禁区)。
|
|
774
|
+
if (deps?.signal?.aborted === true) {
|
|
775
|
+
return { outcome: 'timeout', waitedMs: Date.now() - cancelStartedAt, aborted: true, confirmedHeld: false };
|
|
776
|
+
}
|
|
777
|
+
const delivery = atMostOnceFailureClass(e);
|
|
778
|
+
if (delivery === 'rejected')
|
|
779
|
+
return { outcome: 'failed', detail: describeFailure(e), delivery };
|
|
780
|
+
// 本枪的截止到点(请求挂死被掐)同样是**送达未知** —— 服务端可能已经受理,照走确认腿。
|
|
781
|
+
cancelFailure = { detail: describeFailure(e), delivery };
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
cancelLease.release();
|
|
785
|
+
}
|
|
786
|
+
// 202 = 「收下了」不是「停了」⇒ 必须真等到那条 run 交出会话,否则重发只会再撞一次 409。
|
|
787
|
+
const verdict = await waitForClaimRelease(taskId, {
|
|
788
|
+
get: boundGet,
|
|
789
|
+
budgetMs: cancelWaitMs,
|
|
790
|
+
...(deps?.signal !== undefined ? { signal: deps.signal } : {}),
|
|
791
|
+
...sessionOpts(deps),
|
|
792
|
+
});
|
|
793
|
+
// 会话真被交出来了 ⇒ 就是取消成功,哪怕那一枪的回执丢在路上(决定量说了算)。
|
|
794
|
+
if (verdict.released)
|
|
795
|
+
return { outcome: 'released' };
|
|
796
|
+
if (cancelFailure !== null)
|
|
797
|
+
return { outcome: 'failed', ...cancelFailure };
|
|
798
|
+
return {
|
|
799
|
+
outcome: 'timeout',
|
|
800
|
+
waitedMs: verdict.waitedMs,
|
|
801
|
+
aborted: verdict.aborted,
|
|
802
|
+
confirmedHeld: verdict.lastStatus !== null && CLAIM_HELD_STATES.includes(verdict.lastStatus),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
525
805
|
/**
|
|
526
806
|
* `running` 形:呈三选卡 → 按用户的决定动手。零决定 = 零动作(`not-parked` 现状行)。
|
|
527
807
|
*
|
|
@@ -566,28 +846,16 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
|
|
|
566
846
|
// tsc 控制流分析把第二次窄成恒假比较(TS2367)。
|
|
567
847
|
const probeCallerAborted = () => deps?.signal?.aborted === true;
|
|
568
848
|
if (statusFromWire && typeof runs?.get === 'function' && !probeCallerAborted()) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
fresh = readStatus(await runs.get(taskId, { signal: lease.signal, ...sessionOpts(deps) }));
|
|
575
|
-
probeAnswered = true;
|
|
576
|
-
}
|
|
577
|
-
catch (e) {
|
|
578
|
-
if (e?.status === 404)
|
|
579
|
-
ghost = true;
|
|
580
|
-
}
|
|
581
|
-
finally {
|
|
582
|
-
lease.release();
|
|
583
|
-
}
|
|
584
|
-
if (ghost)
|
|
849
|
+
// 与陈旧 park 臂共用同一只读叶(同形单源,见 {@link probeRunStatusOnce})。
|
|
850
|
+
const probe = await probeRunStatusOnce(taskId, runs, runs.get, deps);
|
|
851
|
+
const fresh = probe.status;
|
|
852
|
+
if (probe.ghost)
|
|
585
853
|
return { kind: 'running-not-found', taskId };
|
|
586
854
|
// (对抗复审 [medium] 采纳)对账期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记)都不做 ——
|
|
587
855
|
// Esc 之后还弹卡/重开 = 用户说停还在动。零动作现状行收口(与呈卡腿抛错同一条诚实收口)。
|
|
588
856
|
if (probeCallerAborted())
|
|
589
857
|
return notParked;
|
|
590
|
-
if (
|
|
858
|
+
if (fresh !== null && !RUNNING_STATES.includes(fresh)) {
|
|
591
859
|
if (PLAN_REVIEW_STATES.includes(fresh))
|
|
592
860
|
return planReviewArm(taskId, busy, deps);
|
|
593
861
|
if (ASK_PARK_STATES.includes(fresh))
|
|
@@ -618,16 +886,10 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
|
|
|
618
886
|
// wait = 用户看过卡并选择留着它(呈现层把 Esc/空答也收口成 wait —— 同样是「这次不动它」的表态);
|
|
619
887
|
// steer/cancel = 用户重新表了态 ⇒ 清登记。呈卡腿抛错那次**不**登记(用户没看到卡)。
|
|
620
888
|
if (choice === 'wait') {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (runningChoiceDeclined.size > RUNNING_CHOICE_LEDGER_CAP) {
|
|
626
|
-
const oldest = runningChoiceDeclined.values().next().value;
|
|
627
|
-
if (oldest !== undefined)
|
|
628
|
-
runningChoiceDeclined.delete(oldest);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
889
|
+
// (对抗复审 [medium] 轻形采纳)FIFO 有界:长命桌面/多会话宿主不许无界长住(叶
|
|
890
|
+
// {@link rememberDeclinedOffer},与 L-93 两选卡共用同一条上界)。
|
|
891
|
+
if (offerDelivered)
|
|
892
|
+
rememberDeclinedOffer(declineKey);
|
|
631
893
|
}
|
|
632
894
|
else {
|
|
633
895
|
runningChoiceDeclined.delete(declineKey);
|
|
@@ -663,68 +925,21 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
|
|
|
663
925
|
}
|
|
664
926
|
}
|
|
665
927
|
if (choice === 'cancel' && canCancel && runs?.cancel !== undefined && runs.get !== undefined) {
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
* 🔴 cancel 的失败也分两类(二次评审 R4 [high]),与 steer 同一把尺:
|
|
672
|
-
* · `rejected`(4xx)—— 服务端答了且明确拒 ⇒ 那一枪确定没生效,立即如实说,不必再等确认腿;
|
|
673
|
-
* · `unknown`(5xx / 连接在回执路上断了)—— 请求**可能已经被受理并在异步执行**。此时直接收口成
|
|
674
|
-
* 「取消失败,那条 run 还占着会话」是对用户下一个证不出的断言 —— 而这条腿手上恰恰有一个能
|
|
675
|
-
* 回答这个问题的读口(`get`)。所以照走有界确认腿:真交出来了就是 `running-cancelled`
|
|
676
|
-
* (用户授权的取消其实成了),确认不到才如实说「没能确认」。判据锚在**决定量**(会话有没有
|
|
677
|
-
* 被交出来)上,不锚它的一个前置条件(那一枪的回执长什么样)。
|
|
678
|
-
*/
|
|
679
|
-
let cancelFailure = null;
|
|
680
|
-
const cancelWaitMs = deps?.cancelReleaseWaitMs ?? CANCEL_RELEASE_WAIT_MS;
|
|
681
|
-
const cancelStartedAt = Date.now();
|
|
682
|
-
const cancelLease = claimProbeLease(deps?.signal, cancelWaitMs);
|
|
683
|
-
try {
|
|
684
|
-
// 🔴 这一枪也必须**带中止口与截止**(二次评审 R5 [high]):`cancel` 的 promise 不落地,后面那条
|
|
685
|
-
// 有界确认腿就根本起不来 —— 用户按了 Esc 既停不掉等待也停不掉出站请求,整条腿零结局。有界性
|
|
686
|
-
// 的执行手段只有一个:把 signal 真交到请求上。
|
|
687
|
-
await durable.cancel?.(taskId, { signal: cancelLease.signal, ...sessionOpts(deps) });
|
|
688
|
-
}
|
|
689
|
-
catch (e) {
|
|
690
|
-
// 用户中止 ⇒ 当拍收口(绝不接着跑一个满窗的确认腿:「UI 说停了、后台还在问」正是禁区)。
|
|
691
|
-
if (deps?.signal?.aborted === true) {
|
|
692
|
-
return {
|
|
693
|
-
kind: 'running-cancel-timeout',
|
|
694
|
-
taskId,
|
|
695
|
-
waitedMs: Date.now() - cancelStartedAt,
|
|
696
|
-
aborted: true,
|
|
697
|
-
confirmedHeld: false,
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
const delivery = atMostOnceFailureClass(e);
|
|
701
|
-
if (delivery === 'rejected') {
|
|
702
|
-
return { kind: 'running-cancel-failed', taskId, detail: describeFailure(e), delivery };
|
|
703
|
-
}
|
|
704
|
-
// 本枪的截止到点(请求挂死被掐)同样是**送达未知** —— 服务端可能已经受理,照走确认腿。
|
|
705
|
-
cancelFailure = { detail: describeFailure(e), delivery };
|
|
706
|
-
}
|
|
707
|
-
finally {
|
|
708
|
-
cancelLease.release();
|
|
709
|
-
}
|
|
710
|
-
// 202 = 「收下了」不是「停了」⇒ 必须真等到那条 run 交出会话,否则重发只会再撞一次 409。
|
|
711
|
-
const verdict = await waitForClaimRelease(taskId, {
|
|
712
|
-
get: boundGet,
|
|
713
|
-
budgetMs: cancelWaitMs,
|
|
714
|
-
...(deps?.signal !== undefined ? { signal: deps.signal } : {}),
|
|
715
|
-
...sessionOpts(deps),
|
|
716
|
-
});
|
|
717
|
-
// 会话真被交出来了 ⇒ 就是取消成功,哪怕那一枪的回执丢在路上(决定量说了算)。
|
|
718
|
-
if (verdict.released)
|
|
928
|
+
// 🔴 **复用**共享的 cancel + 有界释放确认腿(见 {@link cancelAndConfirmRelease});本臂只做
|
|
929
|
+
// 「判决 → 自己这一族的结局名」的映射。L-93 的两选卡走同一条腿 —— 复制第二份的代价是两条腿
|
|
930
|
+
// 的失败分类与释放判据各自漂,而它们说的是同一件事。
|
|
931
|
+
const verdict = await cancelAndConfirmRelease(taskId, runs, runs.get, deps);
|
|
932
|
+
if (verdict.outcome === 'released')
|
|
719
933
|
return { kind: 'running-cancelled', taskId };
|
|
720
|
-
if (
|
|
721
|
-
return { kind: 'running-cancel-failed', taskId,
|
|
934
|
+
if (verdict.outcome === 'failed') {
|
|
935
|
+
return { kind: 'running-cancel-failed', taskId, detail: verdict.detail, delivery: verdict.delivery };
|
|
936
|
+
}
|
|
722
937
|
return {
|
|
723
938
|
kind: 'running-cancel-timeout',
|
|
724
939
|
taskId,
|
|
725
940
|
waitedMs: verdict.waitedMs,
|
|
726
941
|
aborted: verdict.aborted,
|
|
727
|
-
confirmedHeld: verdict.
|
|
942
|
+
confirmedHeld: verdict.confirmedHeld,
|
|
728
943
|
};
|
|
729
944
|
}
|
|
730
945
|
// 用户选了 ③ / 卡没答上 / 选了一条本轮不可兑现的路 ⇒ 零动作,如实转述现状。
|
|
@@ -990,6 +1205,68 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
|
|
|
990
1205
|
return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), so ` +
|
|
991
1206
|
`nothing was cancelled and sema cannot tell whether that run is still holding this session. Your message ` +
|
|
992
1207
|
`was NOT sent; send it again to find out, or ${wayOut}.`);
|
|
1208
|
+
// ── L-93 陈旧 park 的五种结局(0.53.0)────────────────────────────────────────────────────────
|
|
1209
|
+
case 'ask-run-not-found':
|
|
1210
|
+
// 待决行确已不在 + 一手复核撞 404:两个缺席证据指向同一件事,但「已解锁」仍然证不出
|
|
1211
|
+
// (该 404 是 unknown-run ∪ non-owner 的不可分辨并集)⇒ 只说已证的事实 + 「再发一次」这条
|
|
1212
|
+
// 真出路。🔴 不提 decidePath:那张卡已经不在引擎上,POST 过去只会再吃一个 404(假出路)。
|
|
1213
|
+
return (`The previous turn is recorded as parked on a decision (run ${outcome.taskId}), but the approval it ` +
|
|
1214
|
+
`was waiting on is no longer on the engine, and asking the engine about that run directly comes back ` +
|
|
1215
|
+
`not-found — a lookup that cannot separate a run that is really gone from one this session is simply ` +
|
|
1216
|
+
`not allowed to read. Either way sema has nothing it can act on here, so it did NOT cancel anything. ` +
|
|
1217
|
+
`Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
|
|
1218
|
+
case 'stale-park-cancelled':
|
|
1219
|
+
// 🔴 resending 形绝不说「NOT sent」:释放已被引擎确认(判据 = CLAIM_RELEASED_STATES),
|
|
1220
|
+
// 调用方下一步就是重发那条消息,说它没送出去是对着一个正在发生的动作谎报。
|
|
1221
|
+
return (`Run ${outcome.taskId} was parked on an approval that no longer exists on the engine, so there was ` +
|
|
1222
|
+
`nothing left for you to answer. You chose to cancel that run: the engine now reports it as finished, ` +
|
|
1223
|
+
`so it will not resume — whatever that turn had left to do is not coming back. sema is sending your ` +
|
|
1224
|
+
`message again now; if that run has not fully let go of the session yet, you may have to send it once ` +
|
|
1225
|
+
`more.`);
|
|
1226
|
+
case 'stale-park-cancel-timeout': {
|
|
1227
|
+
// 秒数取**真等了多久**(不是预算),与 running-cancel-timeout 同律。
|
|
1228
|
+
const waited = `${String(Math.max(1, Math.round(outcome.waitedMs / 1000)))}s`;
|
|
1229
|
+
if (outcome.aborted) {
|
|
1230
|
+
return (`sema asked the engine to cancel run ${outcome.taskId} (it was parked on an approval that no longer ` +
|
|
1231
|
+
`exists there) and you interrupted while it was still confirming (${waited} in). Cancelling is ` +
|
|
1232
|
+
`asynchronous, so that run may still be winding down and may still hold this session. Your message ` +
|
|
1233
|
+
`was NOT sent; send it again in a moment, or ${wayOut}.`);
|
|
1234
|
+
}
|
|
1235
|
+
if (!outcome.confirmedHeld) {
|
|
1236
|
+
// 一次都没读出那条 run 的状态 ⇒ 只许说「确认不了」,绝不替引擎断言「它还占着」。
|
|
1237
|
+
return (`sema asked the engine to cancel run ${outcome.taskId} (parked on an approval that no longer exists ` +
|
|
1238
|
+
`there) but could not read that run's state back within ${waited}, so it cannot confirm whether the ` +
|
|
1239
|
+
`session was released. Your message was NOT sent; send it again in a moment (if that run really is ` +
|
|
1240
|
+
`gone it will just run), or ${wayOut}.`);
|
|
1241
|
+
}
|
|
1242
|
+
return (`sema asked the engine to cancel run ${outcome.taskId} (parked on an approval that no longer exists ` +
|
|
1243
|
+
`there), but that run still held this session ${waited} later — cancelling is asynchronous and it may ` +
|
|
1244
|
+
`still be winding down. Your message was NOT sent; send it again in a moment, or ${wayOut}.`);
|
|
1245
|
+
}
|
|
1246
|
+
case 'stale-park-cancel-failed':
|
|
1247
|
+
if (outcome.delivery === 'unknown') {
|
|
1248
|
+
// 送达未知 ⇒ 那条 run 可能已经在停了;说「它还占着会话」是替引擎下一个证不出的断言。
|
|
1249
|
+
return (`sema sent the cancel for run ${outcome.taskId} (parked on an approval that no longer exists on the ` +
|
|
1250
|
+
`engine) but could not confirm what happened to it (${outcome.detail}), and it could not read that ` +
|
|
1251
|
+
`run back as released either. That run may or may not still be holding this session. Your message ` +
|
|
1252
|
+
`was NOT sent; send it again in a moment, or ${wayOut}.`);
|
|
1253
|
+
}
|
|
1254
|
+
return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), ` +
|
|
1255
|
+
`so nothing was cancelled and sema cannot tell whether that run is still holding this session. Your ` +
|
|
1256
|
+
`message was NOT sent; send it again to find out, or ${wayOut}.`);
|
|
1257
|
+
case 'stale-park-wait':
|
|
1258
|
+
// 两条真出路都必须在这一行里(压噪不压把手):引擎侧 cancel 端点与宿主的 wayOut。
|
|
1259
|
+
// 🔴 这一行**不**说「答那张卡」—— 那张卡正是已经不在了的东西。
|
|
1260
|
+
if (outcome.alreadyOffered === true) {
|
|
1261
|
+
return (`Run ${outcome.taskId} is still parked (status ${outcome.status}) on an approval that no longer ` +
|
|
1262
|
+
`exists on the engine. sema already asked what to do with it and you chose to leave it alone, so it ` +
|
|
1263
|
+
`is not asking again. Your message was NOT sent; cancel that run on the engine ` +
|
|
1264
|
+
`(POST /v1/runs/${outcome.taskId}/cancel — that ends it for good), or ${wayOut}.`);
|
|
1265
|
+
}
|
|
1266
|
+
return (`Run ${outcome.taskId} is still parked (status ${outcome.status}) on an approval that no longer exists ` +
|
|
1267
|
+
`on the engine, so there is no card left to answer and that run will not settle on its own. sema did ` +
|
|
1268
|
+
`NOT cancel it — you chose to leave it alone. Your message was NOT sent; cancel that run on the engine ` +
|
|
1269
|
+
`(POST /v1/runs/${outcome.taskId}/cancel — that ends it for good), or ${wayOut}.`);
|
|
993
1270
|
case 'running-not-found':
|
|
994
1271
|
// Inkglow-1085 P0b②:幽灵 claim —— 引擎一边说这条 run 占着会话、一边查无此 run。「已解锁」
|
|
995
1272
|
// 证不出(404 与 non-owner 同码刻意不可分辨),所以只说已证的事实 + 「再发一次」这条真出路
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -135,6 +135,13 @@
|
|
|
135
135
|
export * from './seam.js';
|
|
136
136
|
export * from './workflow.js';
|
|
137
137
|
export * from './notifications.js';
|
|
138
|
+
// ── design/385(0.54.0):三条引擎注入车道的类型化投影 + 转录行文本编解码 ────────────────────
|
|
139
|
+
// 收在库里的理由:① 判别位是**类型化载体的在场**(agentMessage / crossSessionMessage /
|
|
140
|
+
// crossSessionNotice),端若各自判就会有人拿 `summary` 文本判 —— 那条路上任何后台任务都能把
|
|
141
|
+
// `<agent-message from="…">` 写进 summary 冒充一条同事消息;② 端的消息面只拿得到**文本**,
|
|
142
|
+
// 所以「帧→文本」与「文本→帧」两半必须同源在包里,端才做得到零字符串判定(cli-191 A-D1/A-D12
|
|
143
|
+
// 那一族的成因正是消费端各自抄一份判据)。
|
|
144
|
+
export * from './peerFrames.js';
|
|
138
145
|
export * from './steering.js';
|
|
139
146
|
export * from './diagnostics.js';
|
|
140
147
|
export * from './retryStatus.js';
|