@sema-agent/client-core 0.15.0 → 0.16.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/README.md +4 -3
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +38 -25
- package/dist/adapter/downstream/terminalToSdkResult.js +107 -58
- package/dist/adapter/runStream.js +12 -8
- package/dist/controlRouter.d.ts +11 -0
- package/dist/controlRouter.js +11 -0
- package/dist/detachWire.d.ts +6 -0
- package/dist/detachWire.js +6 -0
- package/dist/finalVerifyWire.d.ts +5 -0
- package/dist/finalVerifyWire.js +5 -0
- package/dist/fleet/fleetLedger.d.ts +9 -0
- package/dist/fleet/fleetLedger.js +10 -1
- package/dist/fleet/fleetProjection.d.ts +13 -1
- package/dist/fleet/fleetProjection.js +13 -1
- package/dist/fleetAgentPanelProjection.js +25 -3
- package/dist/index.js +10 -0
- package/dist/interactiveToolsWire.d.ts +6 -1
- package/dist/interactiveToolsWire.js +6 -1
- package/dist/limitsWire.d.ts +77 -23
- package/dist/limitsWire.js +108 -35
- package/dist/notifications.d.ts +4 -0
- package/dist/notifications.js +134 -13
- package/dist/sandboxWire.d.ts +12 -1
- package/dist/sandboxWire.js +12 -1
- package/dist/scenarioWire.d.ts +12 -1
- package/dist/scenarioWire.js +12 -1
- package/dist/seatContract.js +25 -5
- package/dist/steering.js +11 -2
- package/dist/subagentContentStore.d.ts +13 -0
- package/package.json +1 -1
package/dist/notifications.js
CHANGED
|
@@ -381,6 +381,19 @@ const outstandingRuns = new Map(); // runId → registeredAt(ms)
|
|
|
381
381
|
// workflow(s) to finish」(CC 2.1.212 同形,wf-ui-cc2 ground truth)。仅通知计数变化 ——
|
|
382
382
|
// notif-03 起「计数」含 `outstandingAbandonedCount()`,故 bg 半场的 TTL 放弃也发一次通知
|
|
383
383
|
// (那一格变了,消费方要重渲「N 个后台任务已停止等待」)。
|
|
384
|
+
//
|
|
385
|
+
// 🔴 [2393] notif-F9(2026-08-03 全窗复审)**判据纠偏 + 契约写清**(裁定:机制不改,把口径写死)。
|
|
386
|
+
// 复审的观察是「bg 半场信号不对称:放弃发通知、登记不发,而订阅面两个计数口都不含 bg ⇒ 一次 bg
|
|
387
|
+
// 放弃叫醒全部订阅者却什么都没变(空转重渲)」。逐口核过之后这条**不成立**,但它指出的真缺口
|
|
388
|
+
// (契约没写清)是真的,所以写在这里:
|
|
389
|
+
// · 订阅契约 = 「**任一可读计数**发生变化」,可读计数恰好三个:`outstandingWorkflowCount()` /
|
|
390
|
+
// `outstandingDeliverableWorkflowCount()` / `outstandingAbandonedCount()`;
|
|
391
|
+
// · bg **放弃**必须发:它改变第三个(`abandonedCount++`)—— 不是空转;
|
|
392
|
+
// · bg **登记**必须不发:bg 在飞数**没有**可读口(刻意:headless 退出门只认 workflow 半场,
|
|
393
|
+
// 见 `outstandingDeliverableWorkflowCount` 的头注),不改变任何可读量的事件不许叫醒订阅者
|
|
394
|
+
// (叫醒 = 每次 async_launched 回执触发一次全树重渲,而消费方一个字都读不到新东西)。
|
|
395
|
+
// ⇒ 「不对称」是这条契约的**正确后果**,不是漏。谁想给 bg 在飞数开一个可读口,必须同批让
|
|
396
|
+
// `registerOutstandingBgTask` 发通知(两半共享前提,[paired-mechanisms-must-share-premise])。
|
|
384
397
|
const outstandingListeners = new Set();
|
|
385
398
|
function notifyOutstanding() {
|
|
386
399
|
for (const l of [...outstandingListeners]) {
|
|
@@ -417,8 +430,20 @@ function abandonOutstanding(kind, id, reason) {
|
|
|
417
430
|
if (!removed)
|
|
418
431
|
return;
|
|
419
432
|
abandonedCount++;
|
|
420
|
-
|
|
421
|
-
'no completion notification will ever be delivered for it'
|
|
433
|
+
const line = `abandoned ${kind} ${id} — reason=${reason}, waited > WATCH_TTL_MS(${WATCH_TTL_MS}ms); ` +
|
|
434
|
+
'no completion notification will ever be delivered for it';
|
|
435
|
+
traceNotif(line);
|
|
436
|
+
// 🔴 [2393] notif-F2(2026-08-03 全窗复审)**边界定谳**:本条修复到库边界为止,用户可见那一端
|
|
437
|
+
// 由宿主闭合,已挂提货单(`docs/refactor/README.md`「提货单 / 破坏性变更清单」节的**宿主侧待办**表,
|
|
438
|
+
// 背景与「库侧为什么停在这里」见 `docs/refactor/WAVE2-RESIDUALS.md` [2393] 那一节第 2 条)。
|
|
439
|
+
// 为什么不在库内补偿:
|
|
440
|
+
// · `traceNotif` 受 SEMA_DEBUG 门控是**刻意**的 —— 那是诊断面;把放弃行为默认打进用户 stderr
|
|
441
|
+
// 等于库替宿主决定 UI(headless `-p` 的 stdout 是协议面,污染它是真回归);
|
|
442
|
+
// · 走 `hostLog` 口需要 `notifications.ts` import `host.ts`,实测会把 **A 层闭包 22→24 文件**
|
|
443
|
+
// (`run-client-core-portability-test.mjs` 的 `MAX_ADAPT_CLOSURE_FILES` 只许降)—— 为一条诊断
|
|
444
|
+
// 线把 A 层拽进宿主口体系,代价大于收益([no-hasty-compensation-final-fix-first] 三问);
|
|
445
|
+
// · 用户可见那一端的**量**已经在场且可读:`outstandingAbandonedCount()`(+ `notificationDropCounters()`
|
|
446
|
+
// 六格)。缺的是三端的渲染,那是宿主工单,不是库内补偿位。
|
|
422
447
|
notifyOutstanding();
|
|
423
448
|
}
|
|
424
449
|
/**
|
|
@@ -431,11 +456,19 @@ export function outstandingAbandonedCount() {
|
|
|
431
456
|
}
|
|
432
457
|
let bgDedupDropped = 0;
|
|
433
458
|
let bgCrossChannelDropped = 0;
|
|
459
|
+
let workflowDedupDropped = 0;
|
|
434
460
|
let stuckTickResets = 0;
|
|
435
461
|
let bgWatchRevivalPromoted = 0;
|
|
436
462
|
let bgWatchCollectedUnprobed = 0;
|
|
437
463
|
export function notificationDropCounters() {
|
|
438
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
bgDedupDropped,
|
|
466
|
+
bgCrossChannelDropped,
|
|
467
|
+
workflowDedupDropped,
|
|
468
|
+
stuckTickResets,
|
|
469
|
+
bgWatchRevivalPromoted,
|
|
470
|
+
bgWatchCollectedUnprobed,
|
|
471
|
+
};
|
|
439
472
|
}
|
|
440
473
|
let statusProbe = null;
|
|
441
474
|
let watchTimer = null;
|
|
@@ -447,6 +480,18 @@ export function installWorkflowStatusProbe(probe) {
|
|
|
447
480
|
}
|
|
448
481
|
/** bg 子代生命周期号的首周期值(SendMessage 复活即 +1;wire 缺 seq ⇒ 按首周期解释)。 */
|
|
449
482
|
const BG_FIRST_SEQ = 1;
|
|
483
|
+
/**
|
|
484
|
+
* 🔴 [2393] notif-F8(2026-08-03 全窗复审):seq 归一的**唯一铸点**。
|
|
485
|
+
* 修前有两处各写各的:登记口 `Number.isFinite && >= BG_FIRST_SEQ` 才收(否则钳到首周期,再 floor),
|
|
486
|
+
* enqueue 口只有 `n.seq ?? BG_FIRST_SEQ`(零校验),而帧臂(`fleet/fleetLedger.ts` 的
|
|
487
|
+
* `applyBgNotification`)是**原样透传**。后果不是「多写一遍」而是两条通道的去重键会**错开**:
|
|
488
|
+
* server 哪天发 0 基 seq(或小数)⇒ watcher 合成走已钳成 1 的键 `id:1:status`,帧通道走原值键
|
|
489
|
+
* `id:0:status`,两键不撞 ⇒ 跨通道去重整体失效,同一次完成被喂给模型两遍。
|
|
490
|
+
* notif-02 立的是「键必须含 seq」,归一没收成单口就等于键的**语义**没收口。
|
|
491
|
+
*/
|
|
492
|
+
function normalizeBgSeq(seq) {
|
|
493
|
+
return typeof seq === 'number' && Number.isFinite(seq) && seq >= BG_FIRST_SEQ ? Math.floor(seq) : BG_FIRST_SEQ;
|
|
494
|
+
}
|
|
450
495
|
/**
|
|
451
496
|
* 🔴 notif-02:观察台账的键是 **(taskId, seq) 复合键**,不是裸 taskId。
|
|
452
497
|
* 裸 taskId 时 seq≥2 的复活周期与首周期同键 ⇒ 复活周期结构性进不了台账,而 watcher 存在的
|
|
@@ -495,7 +540,7 @@ export function registerOutstandingBgTask(taskId, description, prompt, seq) {
|
|
|
495
540
|
if (typeof prompt === 'string' && prompt.length > 0 && !bgTaskPrompts.has(taskId)) {
|
|
496
541
|
bgTaskPrompts.set(taskId, prompt);
|
|
497
542
|
}
|
|
498
|
-
const cycle =
|
|
543
|
+
const cycle = normalizeBgSeq(seq); // notif-F8:唯一铸点
|
|
499
544
|
const key = bgOutstandingKey(taskId, cycle);
|
|
500
545
|
if (outstandingBgTasks.has(key))
|
|
501
546
|
return;
|
|
@@ -595,7 +640,14 @@ function withProbeDeadline(p, timeoutMs) {
|
|
|
595
640
|
const timer = setTimeout(() => {
|
|
596
641
|
reject(new ProbeDeadlineError(timeoutMs));
|
|
597
642
|
}, timeoutMs);
|
|
598
|
-
|
|
643
|
+
// 🔴 [2393] notif-F7(2026-08-03 全窗复审)**不 unref**:这正是 `unrefTimer.ts:6-9` 逐字写死的
|
|
644
|
+
// 域词表-13 定谳所指的那一类 —— 「被 await 的 race/budget 臂上的定时器绝不能 unref(会让进程
|
|
645
|
+
// 在它 settle 前提前退出,RB-447 同形)」。本 deadline 定时器是 `await withProbeDeadline(...)`
|
|
646
|
+
// **唯一的 settle 来源**(probe 永不 settle 时):unref 之后若宿主此刻没有别的活句柄
|
|
647
|
+
// (watchTimer 自己也是 unref 的),事件循环判定可退出,回调不再执行,那一拍 tick 永不 settle
|
|
648
|
+
// —— notif-01① 「必有 settle 路径」的保证就不是自持的,而是押在「宿主别处总有活句柄」上。
|
|
649
|
+
// 两条成对臂已经保证它不会拖住进程:①probe settle 时下面两个 `clearTimeout(timer)` 必执行;
|
|
650
|
+
// ②它自己到点就 reject 并被 clear,寿命上界 = timeoutMs(默认 PROBE_TIMEOUT_MS)。
|
|
599
651
|
p.then(v => {
|
|
600
652
|
clearTimeout(timer);
|
|
601
653
|
resolve(v);
|
|
@@ -616,14 +668,46 @@ let tickGeneration = 0;
|
|
|
616
668
|
* 🔴 必须在重入护栏**之前**跑:清扫住在 tickWatchInner 里时,一个卡死的 probe 会连坐 TTL,
|
|
617
669
|
* 于是「2h 上界」这个 `outstandingDeliverableWorkflowCount()` 正当性的另一半也一起失效。
|
|
618
670
|
*/
|
|
671
|
+
/**
|
|
672
|
+
* 🔴 [2393] notif-F10(2026-08-03 全窗复审):**正被在飞 probe 臂持有**的键集(bg 侧存复合键,
|
|
673
|
+
* workflow 侧存 runId)。`abandonOutstanding` 自己的前提是「放弃必须是**真发生过**的事」,但它
|
|
674
|
+
* 上一版只挡住了「键不在台账」,没挡住「键正被在飞臂持有」:上一拍 probe 还在飞(它手里握着这
|
|
675
|
+
* 条目的快照),这一拍 sweep 判它超 TTL ⇒ 计数 +1 并打出「no completion notification will ever
|
|
676
|
+
* be delivered for it」,几十毫秒后那个 probe 返回 terminal ⇒ 照常合成并投递通知。
|
|
677
|
+
* 留痕是假话,`outstandingAbandonedCount()`(消费方据以渲「N 个后台任务已停止等待」的诚实计数)
|
|
678
|
+
* 被灌一格假数 —— 正是 notif-03 立案要根治的那一类观测面撒谎。
|
|
679
|
+
* 🔴 让位必须**自己有界**,否则它就变成 notif-01③ 刚修掉的那个形(「清扫住在 tick 体内 ⇒ 一个卡死的
|
|
680
|
+
* probe 连坐 TTL ⇒ headless 退出门恒真、进程永不退出」)。所以记的是**在飞臂的起始时刻**,
|
|
681
|
+
* 让位只在「这条臂还没超过它自己的 deadline」时成立:臂一旦活过 `probeTimeoutMs()`(= 它本该被
|
|
682
|
+
* 截断的时刻),它就不再是「马上会给出答案的那条臂」,TTL 照常执行。
|
|
683
|
+
* ⇒ 正常路径:多等一拍(5s),换来放弃计数不被灌假数;病理路径:与修前逐字同形,liveness 不变。
|
|
684
|
+
*/
|
|
685
|
+
const bgProbeInFlight = new Map();
|
|
686
|
+
const runProbeInFlight = new Map();
|
|
687
|
+
/** 在飞臂是否仍在它自己的 deadline 之内(超了就不再让位 —— 见上方 notif-F10 头注)。 */
|
|
688
|
+
function probeArmStillWithinDeadline(startedAt, now) {
|
|
689
|
+
return startedAt !== undefined && now - startedAt <= probeTimeoutMs();
|
|
690
|
+
}
|
|
619
691
|
function sweepExpiredOutstanding(now) {
|
|
620
692
|
for (const [key, meta] of [...outstandingBgTasks]) {
|
|
621
|
-
if (now - meta.registeredAt
|
|
622
|
-
|
|
693
|
+
if (now - meta.registeredAt <= WATCH_TTL_MS)
|
|
694
|
+
continue;
|
|
695
|
+
if (probeArmStillWithinDeadline(bgProbeInFlight.get(key), now)) {
|
|
696
|
+
// notif-F10:在飞臂正握着它、且还没超它自己的 deadline —— 这一拍不判放弃
|
|
697
|
+
// (判了就可能马上被那条臂证伪:计数与留痕当场成假话)。
|
|
698
|
+
traceNotif(`ttl sweep deferred for bg-task ${key}: a probe arm is still in flight within its deadline`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
abandonOutstanding('bg-task', key, 'ttl');
|
|
623
702
|
}
|
|
624
703
|
for (const [runId, registeredAt] of [...outstandingRuns]) {
|
|
625
|
-
if (now - registeredAt
|
|
626
|
-
|
|
704
|
+
if (now - registeredAt <= WATCH_TTL_MS)
|
|
705
|
+
continue;
|
|
706
|
+
if (probeArmStillWithinDeadline(runProbeInFlight.get(runId), now)) {
|
|
707
|
+
traceNotif(`ttl sweep deferred for workflow-run ${runId}: a probe arm is still in flight within its deadline`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
abandonOutstanding('workflow-run', runId, 'ttl');
|
|
627
711
|
}
|
|
628
712
|
}
|
|
629
713
|
/** @param nowMs 时钟注入(仅测试钩用;生产走 Date.now())。 */
|
|
@@ -647,7 +731,7 @@ async function tickWatch(nowMs) {
|
|
|
647
731
|
const myGeneration = ++tickGeneration;
|
|
648
732
|
tickStartedAtMs = now;
|
|
649
733
|
try {
|
|
650
|
-
await tickWatchInner();
|
|
734
|
+
await tickWatchInner(now);
|
|
651
735
|
}
|
|
652
736
|
finally {
|
|
653
737
|
// 被强制复位过就别清:那面时间戳已经属于后来的那一拍了。
|
|
@@ -655,7 +739,9 @@ async function tickWatch(nowMs) {
|
|
|
655
739
|
tickStartedAtMs = null;
|
|
656
740
|
}
|
|
657
741
|
}
|
|
658
|
-
|
|
742
|
+
/** @param now 本拍的时钟(与 `sweepExpiredOutstanding` **同一个来源** —— notif-F10 的让位判据比的是
|
|
743
|
+
* 「在飞臂活了多久 vs 它自己的 deadline」,两边取不同的钟就会得出 3 小时前起飞的荒谬结论)。 */
|
|
744
|
+
async function tickWatchInner(now) {
|
|
659
745
|
// bg agent 半场(与 workflow 半场同节拍;TTL 已提到 tickWatch 的护栏之前统一清扫。
|
|
660
746
|
// probe 未装=mock/离线,只等推送补发)
|
|
661
747
|
const bgProbe = bgStatusProbe;
|
|
@@ -686,6 +772,7 @@ async function tickWatchInner() {
|
|
|
686
772
|
continue;
|
|
687
773
|
}
|
|
688
774
|
let alive;
|
|
775
|
+
bgProbeInFlight.set(key, now); // notif-F10:在飞期间(且未超 deadline)sweep 不许把它判成「放弃」
|
|
689
776
|
try {
|
|
690
777
|
const res = await withProbeDeadline(bgProbe(meta.taskId), probeTimeoutMs());
|
|
691
778
|
if (res === null || res === undefined)
|
|
@@ -695,6 +782,9 @@ async function tickWatchInner() {
|
|
|
695
782
|
catch {
|
|
696
783
|
continue; // 探测失败(含 ProbeDeadlineError)不构成删除证据
|
|
697
784
|
}
|
|
785
|
+
finally {
|
|
786
|
+
bgProbeInFlight.delete(key);
|
|
787
|
+
}
|
|
698
788
|
if (!alive) {
|
|
699
789
|
outstandingBgTasks.delete(key); // 真终局 = 已送达的那个周期,收摊
|
|
700
790
|
continue;
|
|
@@ -713,6 +803,7 @@ async function tickWatchInner() {
|
|
|
713
803
|
}
|
|
714
804
|
if (!bgProbe)
|
|
715
805
|
continue;
|
|
806
|
+
bgProbeInFlight.set(key, now); // notif-F10:同上 —— 在飞臂持有期间(未超 deadline)TTL 让位
|
|
716
807
|
try {
|
|
717
808
|
const res = await withProbeDeadline(bgProbe(meta.taskId), probeTimeoutMs());
|
|
718
809
|
if (res?.terminal) {
|
|
@@ -730,6 +821,9 @@ async function tickWatchInner() {
|
|
|
730
821
|
catch {
|
|
731
822
|
// 单次探测失败(含 ProbeDeadlineError 截断)不放弃:网络抖动照旧,TTL 兜底
|
|
732
823
|
}
|
|
824
|
+
finally {
|
|
825
|
+
bgProbeInFlight.delete(key);
|
|
826
|
+
}
|
|
733
827
|
}
|
|
734
828
|
const probe = statusProbe;
|
|
735
829
|
for (const runId of [...outstandingRuns.keys()]) {
|
|
@@ -740,6 +834,7 @@ async function tickWatchInner() {
|
|
|
740
834
|
}
|
|
741
835
|
if (!probe)
|
|
742
836
|
continue; // live client 未装(mock/离线)→ 只等推送
|
|
837
|
+
runProbeInFlight.set(runId, now); // notif-F10:在飞期间(且未超 deadline)sweep 不许把它判成「放弃」
|
|
743
838
|
try {
|
|
744
839
|
const res = await withProbeDeadline(probe(runId), probeTimeoutMs());
|
|
745
840
|
if (res?.terminal) {
|
|
@@ -755,6 +850,9 @@ async function tickWatchInner() {
|
|
|
755
850
|
catch {
|
|
756
851
|
// 单次探测失败(含 ProbeDeadlineError 截断)不放弃:网络抖动照旧,TTL 兜底
|
|
757
852
|
}
|
|
853
|
+
finally {
|
|
854
|
+
runProbeInFlight.delete(runId);
|
|
855
|
+
}
|
|
758
856
|
}
|
|
759
857
|
}
|
|
760
858
|
/**
|
|
@@ -765,7 +863,7 @@ async function tickWatchInner() {
|
|
|
765
863
|
*/
|
|
766
864
|
const bgNotifiedKeys = new Set();
|
|
767
865
|
export function enqueueBgChildNotification(n) {
|
|
768
|
-
const cycle = n.seq
|
|
866
|
+
const cycle = normalizeBgSeq(n.seq); // notif-F8:与登记口同一个铸点(零校验的 `?? 1` 会让两条通道的键错开)
|
|
769
867
|
const key = `${n.taskId}:${cycle}:${n.status}`;
|
|
770
868
|
// 🔴 notif-03 同族(C5):两条静默 return 都记账 + 留痕 —— 不记账时「按设计去重」与
|
|
771
869
|
// 「seq 丢了导致完成被吞」在观测上完全等价,而后者是用户永远收不到通知的那一类。
|
|
@@ -812,8 +910,17 @@ export function enqueueBgChildNotification(n) {
|
|
|
812
910
|
});
|
|
813
911
|
}
|
|
814
912
|
export function enqueueEngineWorkflowNotification(c) {
|
|
815
|
-
|
|
913
|
+
// 🔴 [2393] notif-F1(2026-08-03 全窗复审):这条静默 return 是 C5 收口的**第三处漏网拼法**。
|
|
914
|
+
// 同批把 bg 侧两条静默 return 都补了计数+留痕(上面 bgDedupDropped / bgCrossChannelDropped),
|
|
915
|
+
// 而它的孪生体 workflow 侧一条都没有 —— 而 `traceNotif` 的头注把执行标准写成「每条丢弃/放弃
|
|
916
|
+
// 路径都能在同一个前缀下 grep 到」。失败场景:`markEngineWorkflowNotified()`(外部通道 seed)
|
|
917
|
+
// 或 probe 链先记账后,推送帧再送同一 runId 的完成 ⇒ 这里静默吞掉,而「按设计去重」与
|
|
918
|
+
// 「一条真完成被误吞(runId 铸法变了 / 两条链 id 不同域)」在观测面上完全等价。
|
|
919
|
+
if (notifiedRunIds.has(c.runId)) {
|
|
920
|
+
workflowDedupDropped++;
|
|
921
|
+
traceNotif(`workflow notification suppressed (run already notified) runId=${c.runId} status=${c.status}`);
|
|
816
922
|
return;
|
|
923
|
+
}
|
|
817
924
|
// [2393] F-1:workflow 侧没有周期概念,周期维按首周期记(与 markEngineWorkflowNotified 同理)。
|
|
818
925
|
markRunNotified(c.runId);
|
|
819
926
|
cardEnqueuedRunIds.add(c.runId);
|
|
@@ -838,12 +945,26 @@ export function _resetEngineTaskNotificationForTest() {
|
|
|
838
945
|
ownWorkflowRuns.clear();
|
|
839
946
|
bgNotifiedKeys.clear();
|
|
840
947
|
outstandingListeners.clear();
|
|
948
|
+
// 🔴 [2393] notif-F11(2026-08-03 全窗复审):这三件此前漏清 —— 头注自称「清空**全部**通知台账」。
|
|
949
|
+
// · `bgTaskPrompts`:首值粘性(已登记的 taskId 不覆写),门里两个用例复用同一 taskId 时
|
|
950
|
+
// `outstandingBgTaskPrompt()` 会跨用例返回上一用例的 prompt,reset 拿不掉(既有钉侥幸没撞上,
|
|
951
|
+
// 不是被挡住了)。它的 process-lifetime 无逐出是**生产**决定(bg 子代 = 人手派发量级),
|
|
952
|
+
// 与测试钩要不要清是两件事。
|
|
953
|
+
// · `tickGeneration`:世代号不复位,跨用例累加(今天只做相等比较,不是现症;但「清空全部」
|
|
954
|
+
// 这句话要么成立要么改掉,不留半真)。
|
|
955
|
+
// · `bgProbeInFlight` / `runProbeInFlight`(notif-F10 新增):上一个用例的 probe 若被用例
|
|
956
|
+
// 提前收尾抛下,残留的键会让下一个用例的 TTL sweep 永远让位 = 静默失效的 TTL。
|
|
957
|
+
bgTaskPrompts.clear();
|
|
958
|
+
bgProbeInFlight.clear();
|
|
959
|
+
runProbeInFlight.clear();
|
|
960
|
+
tickGeneration = 0;
|
|
841
961
|
statusProbe = null;
|
|
842
962
|
bgStatusProbe = null;
|
|
843
963
|
tickStartedAtMs = null;
|
|
844
964
|
abandonedCount = 0;
|
|
845
965
|
bgDedupDropped = 0;
|
|
846
966
|
bgCrossChannelDropped = 0;
|
|
967
|
+
workflowDedupDropped = 0;
|
|
847
968
|
stuckTickResets = 0;
|
|
848
969
|
bgWatchRevivalPromoted = 0;
|
|
849
970
|
bgWatchCollectedUnprobed = 0;
|
package/dist/sandboxWire.d.ts
CHANGED
|
@@ -18,7 +18,18 @@ export type SandboxParseResult = {
|
|
|
18
18
|
* `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
|
|
19
19
|
* Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
|
|
20
20
|
* a mis-typed environment choice must not quietly run on the wrong image).
|
|
21
|
-
|
|
21
|
+
*
|
|
22
|
+
* 🔴 [2393] sweep-F10(2026-08-03 全窗复审)**承诺范围如实收窄**(裁定:行为不改,话改):
|
|
23
|
+
* 「fail-LOUD,never a silent default」这句话现在**不**覆盖一种输入 —— 同一个旗**重复出现**且
|
|
24
|
+
* 前一次缺值/非法、后一次合法(`--sandbox --sandbox foo`):
|
|
25
|
+
* 收编前是 per-occurrence eager validate,在**第一个**缺值 occurrence 上就 fail-loud;收编后走
|
|
26
|
+
* `lastFlagValue`('invalidate' 缺省)= 后一次合法 occurrence 覆盖前一次,整体接受。
|
|
27
|
+
* 为什么裁定不改回去:本模块自己写着的契约就是「Repeated flag ⇒ LAST wins(commander convention)」,
|
|
28
|
+
* eager-validate 违反的正是这条契约(它让**被覆盖掉的**那一次决定整条命令的成败)。
|
|
29
|
+
* 剩下的形(唯一一次出现却缺值 / 值是旗形 / 空 / 超长 / 带空白)**仍然全部 fail-loud** —— 那才是
|
|
30
|
+
* 「打错一个字就悄悄跑在别的镜像上」真正要防的那一族。
|
|
31
|
+
* ⚠️ `docs/REFACTOR-LEDGER.md` REF-CC-143 的处置列此前写的是「缺值策略**统一** fail-loud」,
|
|
32
|
+
* 与落地的两策略叶(`'invalidate'` / `'latch-previous'`)相悖,已同批改正(sweep-F11)。 */
|
|
22
33
|
export declare function parseSandboxArgv(argv: readonly string[]): SandboxParseResult;
|
|
23
34
|
/** The request-body stamp for a selection: EXPLICIT ⇒ `{sandboxImageProfile}`, default/auto ⇒ nothing
|
|
24
35
|
* (auto is advisory-by-prompt — the submit itself never carries a profile, per clay's c) 口径). */
|
package/dist/sandboxWire.js
CHANGED
|
@@ -37,7 +37,18 @@ const USAGE_HINT = "sema: --sandbox expects a profile name or 'auto'.\n" +
|
|
|
37
37
|
* `--sandbox=<v>` forms). No flag ⇒ `{mode:'default'}`. Repeated flag ⇒ LAST wins (commander convention).
|
|
38
38
|
* Missing value / flag-shaped value / empty / over-long ⇒ fail-LOUD parse error (never a silent default —
|
|
39
39
|
* a mis-typed environment choice must not quietly run on the wrong image).
|
|
40
|
-
|
|
40
|
+
*
|
|
41
|
+
* 🔴 [2393] sweep-F10(2026-08-03 全窗复审)**承诺范围如实收窄**(裁定:行为不改,话改):
|
|
42
|
+
* 「fail-LOUD,never a silent default」这句话现在**不**覆盖一种输入 —— 同一个旗**重复出现**且
|
|
43
|
+
* 前一次缺值/非法、后一次合法(`--sandbox --sandbox foo`):
|
|
44
|
+
* 收编前是 per-occurrence eager validate,在**第一个**缺值 occurrence 上就 fail-loud;收编后走
|
|
45
|
+
* `lastFlagValue`('invalidate' 缺省)= 后一次合法 occurrence 覆盖前一次,整体接受。
|
|
46
|
+
* 为什么裁定不改回去:本模块自己写着的契约就是「Repeated flag ⇒ LAST wins(commander convention)」,
|
|
47
|
+
* eager-validate 违反的正是这条契约(它让**被覆盖掉的**那一次决定整条命令的成败)。
|
|
48
|
+
* 剩下的形(唯一一次出现却缺值 / 值是旗形 / 空 / 超长 / 带空白)**仍然全部 fail-loud** —— 那才是
|
|
49
|
+
* 「打错一个字就悄悄跑在别的镜像上」真正要防的那一族。
|
|
50
|
+
* ⚠️ `docs/REFACTOR-LEDGER.md` REF-CC-143 的处置列此前写的是「缺值策略**统一** fail-loud」,
|
|
51
|
+
* 与落地的两策略叶(`'invalidate'` / `'latch-previous'`)相悖,已同批改正(sweep-F11)。 */
|
|
41
52
|
export function parseSandboxArgv(argv) {
|
|
42
53
|
// REF-CC-143(dup-07)收编:extraction 改吃共用 lastFlagValue(argvFlagValue.ts)——scan-all-then-
|
|
43
54
|
// validate-once, matching this module's own documented "Repeated flag ⇒ LAST wins" contract (the
|
package/dist/scenarioWire.d.ts
CHANGED
|
@@ -46,7 +46,18 @@ export type ScenarioParseResult = {
|
|
|
46
46
|
* flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
|
|
47
47
|
* fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
|
|
48
48
|
* the wrong tool surface; same stance as `--sandbox`).
|
|
49
|
-
|
|
49
|
+
*
|
|
50
|
+
* 🔴 [2393] sweep-F10(2026-08-03 全窗复审)**承诺范围如实收窄**(裁定:行为不改,话改):
|
|
51
|
+
* 「fail-LOUD,never a silent default」这句话现在**不**覆盖一种输入 —— 同一个旗**重复出现**且
|
|
52
|
+
* 前一次缺值/非法、后一次合法(`--scenario --scenario foo`):
|
|
53
|
+
* 收编前是 per-occurrence eager validate,在**第一个**缺值 occurrence 上就 fail-loud;收编后走
|
|
54
|
+
* `lastFlagValue`('invalidate' 缺省)= 后一次合法 occurrence 覆盖前一次,整体接受。
|
|
55
|
+
* 为什么裁定不改回去:本模块自己写着的契约就是「Repeated flag ⇒ LAST wins(commander convention)」,
|
|
56
|
+
* eager-validate 违反的正是这条契约(它让**被覆盖掉的**那一次决定整条命令的成败)。
|
|
57
|
+
* 剩下的形(唯一一次出现却缺值 / 值是旗形 / 空 / 超长 / 带空白)**仍然全部 fail-loud** —— 那才是
|
|
58
|
+
* 「打错一个字就悄悄跑在别的镜像上」真正要防的那一族。
|
|
59
|
+
* ⚠️ `docs/REFACTOR-LEDGER.md` REF-CC-143 的处置列此前写的是「缺值策略**统一** fail-loud」,
|
|
60
|
+
* 与落地的两策略叶(`'invalidate'` / `'latch-previous'`)相悖,已同批改正(sweep-F11)。 */
|
|
50
61
|
export declare function parseScenarioArgv(argv: readonly string[]): ScenarioParseResult;
|
|
51
62
|
/**
|
|
52
63
|
* The settings-lane default (`SEMA_HEADLESS_SCENARIO`). FAIL-SOFT on a bad value (stderr warning +
|
package/dist/scenarioWire.js
CHANGED
|
@@ -44,7 +44,18 @@ const USAGE_HINT = "sema: --scenario expects a scenario name (lowercase letters/
|
|
|
44
44
|
* flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
|
|
45
45
|
* fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
|
|
46
46
|
* the wrong tool surface; same stance as `--sandbox`).
|
|
47
|
-
|
|
47
|
+
*
|
|
48
|
+
* 🔴 [2393] sweep-F10(2026-08-03 全窗复审)**承诺范围如实收窄**(裁定:行为不改,话改):
|
|
49
|
+
* 「fail-LOUD,never a silent default」这句话现在**不**覆盖一种输入 —— 同一个旗**重复出现**且
|
|
50
|
+
* 前一次缺值/非法、后一次合法(`--scenario --scenario foo`):
|
|
51
|
+
* 收编前是 per-occurrence eager validate,在**第一个**缺值 occurrence 上就 fail-loud;收编后走
|
|
52
|
+
* `lastFlagValue`('invalidate' 缺省)= 后一次合法 occurrence 覆盖前一次,整体接受。
|
|
53
|
+
* 为什么裁定不改回去:本模块自己写着的契约就是「Repeated flag ⇒ LAST wins(commander convention)」,
|
|
54
|
+
* eager-validate 违反的正是这条契约(它让**被覆盖掉的**那一次决定整条命令的成败)。
|
|
55
|
+
* 剩下的形(唯一一次出现却缺值 / 值是旗形 / 空 / 超长 / 带空白)**仍然全部 fail-loud** —— 那才是
|
|
56
|
+
* 「打错一个字就悄悄跑在别的镜像上」真正要防的那一族。
|
|
57
|
+
* ⚠️ `docs/REFACTOR-LEDGER.md` REF-CC-143 的处置列此前写的是「缺值策略**统一** fail-loud」,
|
|
58
|
+
* 与落地的两策略叶(`'invalidate'` / `'latch-previous'`)相悖,已同批改正(sweep-F11)。 */
|
|
48
59
|
export function parseScenarioArgv(argv) {
|
|
49
60
|
// REF-CC-143(dup-07)收编:extraction 改吃共用 lastFlagValue(argvFlagValue.ts)——scan-all-then-
|
|
50
61
|
// validate-once, matching this function's own documented "Repeated flag ⇒ LAST wins" contract (the
|
package/dist/seatContract.js
CHANGED
|
@@ -73,6 +73,26 @@
|
|
|
73
73
|
* @sema-agent/client-core), the contract shape does not.
|
|
74
74
|
*/
|
|
75
75
|
const bad = (reason, field) => field === undefined ? { reason } : { reason, field };
|
|
76
|
+
/**
|
|
77
|
+
* 🔴 [2393] sweep-F8(2026-08-03 全窗复审):**嵌套失败的 field 路径不许在汇聚点被丢掉**。
|
|
78
|
+
* 修前三种写法各写各的:`chrome` 那条把已经算好的 `chrome.laneProof.lane` 整个覆盖成 `"chrome"`;
|
|
79
|
+
* `request`/`session`/`results[i]`/`sessions[i]` 把路径丢成父键名(reason 前缀里还留着索引,
|
|
80
|
+
* field 里没有)。这与 `SeatValidationFailure.field` 自己的定义(「嵌套位用点号,如
|
|
81
|
+
* `chrome.laneProof`」)直接相悖,也削弱 REF-CC-069 存在的理由 ——「一条被门吃掉的事件从此可诊断」:
|
|
82
|
+
* 宿主日志只会说「chrome 有问题」,而门明明知道是 `chrome.laneProof.lane`。
|
|
83
|
+
* @param prefix 父位路径(`"chrome"` / `` `results[${i}]` ``)
|
|
84
|
+
* @param verdict 子判据的失败(field 可能已是点号路径,也可能是裸键名,也可能缺席)
|
|
85
|
+
*/
|
|
86
|
+
const nested = (prefix, verdict) => {
|
|
87
|
+
// 子判据自己已经带上父前缀时(checkSeatChromeEnvelope 收的就是 field 参数)不再重复拼。
|
|
88
|
+
const path = verdict.field === undefined
|
|
89
|
+
? prefix
|
|
90
|
+
: verdict.field === prefix || verdict.field.startsWith(`${prefix}.`)
|
|
91
|
+
? verdict.field
|
|
92
|
+
: `${prefix}.${verdict.field}`;
|
|
93
|
+
const reason = verdict.reason.startsWith(`${prefix}:`) ? verdict.reason : `${prefix}: ${verdict.reason}`;
|
|
94
|
+
return bad(reason, path);
|
|
95
|
+
};
|
|
76
96
|
/** 存在性判定(§B10:`!!v` 那种真值判定会把空串/0 当缺席,对象位则会把 `{}` 当「有证」)。 */
|
|
77
97
|
const isObject = (v) => typeof v === "object" && v !== null;
|
|
78
98
|
const isAbsent = (v) => typeof v === "undefined";
|
|
@@ -318,7 +338,7 @@ const checkLocalSessionEvent = (v) => {
|
|
|
318
338
|
if (!isAbsent(v.request)) {
|
|
319
339
|
const verdict = checkToolPermissionRequest(v.request);
|
|
320
340
|
if (verdict !== true)
|
|
321
|
-
return
|
|
341
|
+
return nested("request", verdict); // sweep-F8:路径不丢
|
|
322
342
|
}
|
|
323
343
|
if (!optionalString(v.data))
|
|
324
344
|
return bad("data must be a string when present", "data");
|
|
@@ -336,7 +356,7 @@ const checkLocalSessionEvent = (v) => {
|
|
|
336
356
|
if (!isAbsent(v.session)) {
|
|
337
357
|
const verdict = checkLocalSessionRecord(v.session);
|
|
338
358
|
if (verdict !== true)
|
|
339
|
-
return
|
|
359
|
+
return nested("session", verdict); // sweep-F8:路径不丢
|
|
340
360
|
}
|
|
341
361
|
if (!optionalString(v.userMessageUuid))
|
|
342
362
|
return bad("userMessageUuid must be a string when present", "userMessageUuid");
|
|
@@ -352,7 +372,7 @@ const checkLocalSessionEvent = (v) => {
|
|
|
352
372
|
if (!isAbsent(v.chrome)) {
|
|
353
373
|
const verdict = checkSeatChromeEnvelope(v.chrome, "chrome");
|
|
354
374
|
if (verdict !== true)
|
|
355
|
-
return
|
|
375
|
+
return nested("chrome", verdict); // sweep-F8:门算好的 chrome.laneProof.lane 不许被覆盖
|
|
356
376
|
}
|
|
357
377
|
return true;
|
|
358
378
|
};
|
|
@@ -469,7 +489,7 @@ const checkSessionSearchEnvelope = (v) => {
|
|
|
469
489
|
for (let i = 0; i < results.length; i += 1) {
|
|
470
490
|
const verdict = checkSessionSearchResult(results[i]);
|
|
471
491
|
if (verdict !== true)
|
|
472
|
-
return
|
|
492
|
+
return nested(`results[${i}]`, verdict); // sweep-F8
|
|
473
493
|
}
|
|
474
494
|
return true;
|
|
475
495
|
};
|
|
@@ -482,7 +502,7 @@ const checkSessionsState = (v) => {
|
|
|
482
502
|
for (let i = 0; i < sessions.length; i += 1) {
|
|
483
503
|
const verdict = checkLocalSessionRecord(sessions[i]);
|
|
484
504
|
if (verdict !== true)
|
|
485
|
-
return
|
|
505
|
+
return nested(`sessions[${i}]`, verdict); // sweep-F8
|
|
486
506
|
}
|
|
487
507
|
return true;
|
|
488
508
|
};
|
package/dist/steering.js
CHANGED
|
@@ -72,12 +72,21 @@ function parseBackgroundTaskLine(line) {
|
|
|
72
72
|
const m = /^- \[([^\]]+)\] Task "([\s\S]*)$/.exec(head);
|
|
73
73
|
if (!m)
|
|
74
74
|
return null;
|
|
75
|
+
// 🔴 [2393] sweep-F16(2026-08-03 全窗复审):这里**不许**回落成空串。两个捕获组都是非可选的,
|
|
76
|
+
// `m` 非空时它们必在场(所以这条早退今天不可达),但 `?? ''` 的**形**是「不知道就渲空串」——
|
|
77
|
+
// 与同窗刚立的口径(`fleetAgentPanelProjection`「缺席就是 undefined,这里不许兜 0」/
|
|
78
|
+
// `seatContract`「绝不回落成 0(那会渲成 1970)」)相反,且真出问题时会静默产出一条 taskId
|
|
79
|
+
// 为空串的任务行(谁也定位不到)。noUncheckedIndexedAccess 适配的正解是**如实拒绝**。
|
|
80
|
+
const taskId = m[1];
|
|
81
|
+
const description = m[2];
|
|
82
|
+
if (taskId === undefined || description === undefined)
|
|
83
|
+
return null;
|
|
75
84
|
return {
|
|
76
85
|
type: 'task_status',
|
|
77
|
-
taskId
|
|
86
|
+
taskId,
|
|
78
87
|
taskType: 'local_bash',
|
|
79
88
|
status,
|
|
80
|
-
description
|
|
89
|
+
description,
|
|
81
90
|
deltaSummary: null,
|
|
82
91
|
};
|
|
83
92
|
}
|
|
@@ -44,6 +44,19 @@ export type SubagentContentItem = {
|
|
|
44
44
|
kind: 'echo';
|
|
45
45
|
text: string;
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* 🔴 [2393] sweep-F15(2026-08-03 全窗复审)**记一笔口径分叉**(裁定:留 `| undefined`,写明为什么):
|
|
49
|
+
* 下面五个可选位在 eopt 收官批被写成 `?: T | undefined`(= 允许显式赋 `undefined` 落键),而同窗
|
|
50
|
+
* 另两处的口径是相反的(`fleetAgentPanelProjection` 的「🔴 缺席即不落键(消费端据键在不在判三态)」、
|
|
51
|
+
* `toolResult` 的「公面键恒在形不许被 eopt 批翻成条件展开」)。两种口径都出自同一批,当时没有一处记账。
|
|
52
|
+
* 为什么这里走另一条:**本类型的消费端一处都不用「键在不在」判三态** —— 本文件 `apply()` 全部用
|
|
53
|
+
* `typeof ev.delta === 'string'` / `ev.output ?? ''` / `ev.isError === true` 这类**值级**判据
|
|
54
|
+
* (逐行核过,见下方 switch),所以 `{delta: undefined}` 与「不落键」在本模块里可观测等价;而生产者
|
|
55
|
+
* 是宿主的帧适配腿,`{ delta: maybe }` 这种直写形在 eopt 下会编译红,逼它们改写成条件展开只是把
|
|
56
|
+
* 噪音推给三端,换不来任何判别力。
|
|
57
|
+
* ⚠️ 反过来的约束:**谁想给这个类型加一个用「键在不在」判三态的位,就必须同批把这里改成条件展开**
|
|
58
|
+
* (那时两种口径就不能再共存了)。
|
|
59
|
+
*/
|
|
47
60
|
export interface SubagentContentEvent {
|
|
48
61
|
type: 'text_delta' | 'reasoning_delta' | 'tool_start' | 'tool_end';
|
|
49
62
|
taskId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|