@sema-agent/client-core 0.43.1 → 0.44.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 +34 -0
- package/README.md +1 -1
- package/dist/engineCapsCache.d.ts +14 -0
- package/dist/engineCapsCache.js +21 -0
- package/dist/hitl/toolApprovalWire.d.ts +58 -11
- package/dist/hitl/toolApprovalWire.js +138 -6
- package/dist/hooksWireCaps.d.ts +37 -0
- package/dist/hooksWireCaps.js +49 -0
- package/dist/notifications.d.ts +62 -1
- package/dist/notifications.js +68 -0
- package/docs/INTEGRATION-CLIENTS.md +10 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -28,6 +28,40 @@
|
|
|
28
28
|
> 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
|
|
29
29
|
> `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
|
|
30
30
|
|
|
31
|
+
## 0.44.0(2026-08-26)
|
|
32
|
+
|
|
33
|
+
**批面**:server 7.48.0 消费批四件(全 additive;实现以 **7.48.0 fixture dist 真字节**为准,
|
|
34
|
+
发布帖 [5317] 三处帖文不符之处均按 dist 施工,详见各件)。
|
|
35
|
+
|
|
36
|
+
### ① `persistedRuleAnchors` 读口(P-39 缺口转已解)
|
|
37
|
+
|
|
38
|
+
载体 = `POST …/decide` **200 响应体**(`ToolApprovalRespondAckView` 补位),**不是**帧键
|
|
39
|
+
(帖文写帧面,dist 直证在 respond ack 体)。`readPersistedRuleAnchors`:
|
|
40
|
+
- `PersistedRuleAnchor` 三键(`offerIndex`/`memberIndex`/`rule`)**整只判形**——任一键缺/型错,
|
|
41
|
+
整条锚丢弃(绝不半只上屏);
|
|
42
|
+
- 与 `persistedRules` **条数一致门**:两数组条数不等 ⇒ 锚整段丢(锚是 rules 的注脚,
|
|
43
|
+
对不上号的注脚比没有注脚更糟);
|
|
44
|
+
- 矛盾形连坐:单数 `persistedRule` 与批臂同场 ⇒ 整段丢(additive 键绝不削弱既有位语义);
|
|
45
|
+
- 锚文本与 `persistedRules` 逐位同文本核验。
|
|
46
|
+
|
|
47
|
+
### ② `fleet.hookFailureNotice` 投影口
|
|
48
|
+
|
|
49
|
+
- 能力位**嵌在 `capabilities.fleet` 对象里**(平铺读恒 false)——新读口 `engineCapNestedTrue`;
|
|
50
|
+
- `classifyHookFailureFrame` 新口(独立导出,不并姊妹口返回联合);
|
|
51
|
+
- `hook_notice` 第二 kind 九键按 dist 实证:事件位叫 **`event`**(帖文写 `hookEvent`,dist 为准)、
|
|
52
|
+
`reason` 必填闭集 4 词、`detail` 可选,`MAX_HOOK_NOTICE_TEXT_CHARS=512` 管三个文本位。
|
|
53
|
+
|
|
54
|
+
### ③ `rule_material_absent` 入 `REFUSAL_GLOSS`
|
|
55
|
+
|
|
56
|
+
机读码恒随人话上屏(开集透传不变,未知码照旧原样过境)。
|
|
57
|
+
|
|
58
|
+
### ④ `ApprovalCardRequest.toolCallId`(#348)
|
|
59
|
+
|
|
60
|
+
additive 补键+帧→卡透传;`AskDecisionBody` 是 `.strict()` 且无 `persistRule` 键 ⇒
|
|
61
|
+
**不镜像**该键防假 affordance。
|
|
62
|
+
|
|
63
|
+
**测试**:32 门绿;codex 两轮 6 findings 5 修 1 登记;8 变异全抓红(含条数一致门关停变异)。
|
|
64
|
+
|
|
31
65
|
## 0.43.1(2026-08-25)
|
|
32
66
|
|
|
33
67
|
**批面**:`readCcImportRedeemCounts` —— cc-import redeem 200 体的兑付计数**双代归一读口**
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.44.0
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -68,6 +68,20 @@ export declare function engineCapState(baseUrl: string | undefined, key: string)
|
|
|
68
68
|
* 🔴 **语义一字不变**(#318 件③ 三态化后改为经 {@link engineCapState} 单源实现):放行面读的就是
|
|
69
69
|
* 这一口,四种「不知道」继续一律折成 `false` = fail-closed。要分辨成因请读 {@link engineCapState}。 */
|
|
70
70
|
export declare function engineCapTrue(baseUrl: string | undefined, key: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* 同步读口(**嵌套**布尔键):`caps[parentKey][key] === true`。未判/父键缺席/父键非对象(server 用
|
|
73
|
+
* `false` 表示整族不供)/子键缺席/非 true ⇒ **false** = fail-closed,与 {@link engineCapTrue} 同规。
|
|
74
|
+
*
|
|
75
|
+
* 为什么要有它:`/v1/capabilities` 的一部分能力是**分组**发的,而不是全平铺 —— 例如 server 7.48.0 的
|
|
76
|
+
* `fleet: { stream, sessionScope, observe, resume, bgNotifyFailClosed, steerPriority, hookFailureNotice }`
|
|
77
|
+
* (`dist/http/routes/capabilities.js:52`),整族不供时该键**整个是 `false`** 而不是一个空对象。
|
|
78
|
+
* 拿平铺口 {@link engineCapTrue} 去读这种键恒 false(白读),而白读的后果不是报错、是**安静地把新
|
|
79
|
+
* 车道判成没有**(藏功能)—— 与本模块头注记的那次缓存失效事故同一种坏法。
|
|
80
|
+
*
|
|
81
|
+
* 🔴 `false` 父键与「父键在场但子键缺席」**都折 false**:两者对放行面是同一件事(这台引擎不供)。
|
|
82
|
+
* 要分辨成因请自己读 caps 对象;本口只答放行问题。
|
|
83
|
+
*/
|
|
84
|
+
export declare function engineCapNestedTrue(baseUrl: string | undefined, parentKey: string, key: string): boolean;
|
|
71
85
|
/**
|
|
72
86
|
* 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
|
|
73
87
|
*
|
package/dist/engineCapsCache.js
CHANGED
|
@@ -171,6 +171,27 @@ export function engineCapState(baseUrl, key) {
|
|
|
171
171
|
export function engineCapTrue(baseUrl, key) {
|
|
172
172
|
return engineCapState(baseUrl, key) === 'true';
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* 同步读口(**嵌套**布尔键):`caps[parentKey][key] === true`。未判/父键缺席/父键非对象(server 用
|
|
176
|
+
* `false` 表示整族不供)/子键缺席/非 true ⇒ **false** = fail-closed,与 {@link engineCapTrue} 同规。
|
|
177
|
+
*
|
|
178
|
+
* 为什么要有它:`/v1/capabilities` 的一部分能力是**分组**发的,而不是全平铺 —— 例如 server 7.48.0 的
|
|
179
|
+
* `fleet: { stream, sessionScope, observe, resume, bgNotifyFailClosed, steerPriority, hookFailureNotice }`
|
|
180
|
+
* (`dist/http/routes/capabilities.js:52`),整族不供时该键**整个是 `false`** 而不是一个空对象。
|
|
181
|
+
* 拿平铺口 {@link engineCapTrue} 去读这种键恒 false(白读),而白读的后果不是报错、是**安静地把新
|
|
182
|
+
* 车道判成没有**(藏功能)—— 与本模块头注记的那次缓存失效事故同一种坏法。
|
|
183
|
+
*
|
|
184
|
+
* 🔴 `false` 父键与「父键在场但子键缺席」**都折 false**:两者对放行面是同一件事(这台引擎不供)。
|
|
185
|
+
* 要分辨成因请自己读 caps 对象;本口只答放行问题。
|
|
186
|
+
*/
|
|
187
|
+
export function engineCapNestedTrue(baseUrl, parentKey, key) {
|
|
188
|
+
if (!baseUrl)
|
|
189
|
+
return false;
|
|
190
|
+
const parent = capsByBase.get(baseUrl)?.[parentKey];
|
|
191
|
+
if (parent === null || typeof parent !== 'object')
|
|
192
|
+
return false;
|
|
193
|
+
return parent[key] === true;
|
|
194
|
+
}
|
|
174
195
|
/**
|
|
175
196
|
* 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
|
|
176
197
|
*
|
|
@@ -296,6 +296,22 @@ export interface ApprovalCardRequest {
|
|
|
296
296
|
toolName: string;
|
|
297
297
|
args: object;
|
|
298
298
|
callKey: string;
|
|
299
|
+
/**
|
|
300
|
+
* #348([5298] 定谳;server ≥7.48.0 帧位早在场)—— **core 的 tool-call id**,消费端用它把审批卡
|
|
301
|
+
* 锚到助手消息里那个 `tool_use` 块。
|
|
302
|
+
*
|
|
303
|
+
* 🔴 **与 {@link ApprovalCardRequest.callKey} 不同义,别互相顶替**:`callKey` 是本包铸的**卡身份
|
|
304
|
+
* 键**(呈现台账/重开腿的主键,live 帧腿 = `hitl-frame:${approvalId}`);本位是**引擎侧的工具调用
|
|
305
|
+
* 身份**,是卡与 transcript 里那个 `tool_use` 块之间**唯一**的 wire 事实锚。
|
|
306
|
+
* 🔴 **为什么非它不可**(server `dist/tool-approval.d.ts:64` JSDoc 逐字记载的 #348 病形):此前帧上
|
|
307
|
+
* 只有 `approvalId`,而那个 id 之后**再没有任何帧提到过** ⇒ 宿主只能拿它当 key 铸卡,卡永远停在
|
|
308
|
+
* awaiting_approval。「拿最近一次 `tool_start` 当锚」这类补偿**不成立** —— 审批帧比 `tool_start`
|
|
309
|
+
* **先到**(cli 帧级取证 4020ms vs 4142ms)。
|
|
310
|
+
* 🔴 **缺席 = 老引擎(≤7.47)或闭合帧**,不是「这次没有工具调用」:消费端必须按
|
|
311
|
+
* [provenance-needs-wire-fact] 降级 —— 有本位就用它 join,缺席则回落既有 `callKey` 台账推断,
|
|
312
|
+
* **绝不编造**一个 id。
|
|
313
|
+
*/
|
|
314
|
+
toolCallId?: string;
|
|
299
315
|
signal?: AbortSignal;
|
|
300
316
|
/**
|
|
301
317
|
* 后台子代 ask 的归属徽章(卡头 `· @name`;宿主 ask 恒缺席)。
|
|
@@ -888,19 +904,50 @@ export interface ToolApprovalRespondAckView extends ToolApprovalRespondAck {
|
|
|
888
904
|
* 自己只批了一条。非数组 / 含非串成员 / 空数组 ⇒ 整只降缺席(半份清单比没有清单更坏)。
|
|
889
905
|
* 非批臂 / 兑付失败 / 老 server ⇒ 字段省略;**缺席 ≠ 空**。
|
|
890
906
|
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
* (
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
907
|
+
* ✅ **P-39 已到货**(server 7.48.0,client-core 本批):上游补的 additive 锚 =
|
|
908
|
+
* {@link ToolApprovalRespondAckView.persistedRuleAnchors}(每条带 `offerIndex` + `memberIndex`)。
|
|
909
|
+
* 本注按 `probeCause` 先例跟批收紧 —— 下面这段记的是**锚缺席时**(≤7.47 引擎)仍然成立的残余:
|
|
910
|
+
* 🔴 **锚缺席 ⇒ 逐条归属证不出**(异源对抗复审三轮 [medium] 登记的原始形)。那种情形下本包对这一位
|
|
911
|
+
* 做到的相关性只有 **id + decision + 基数**({@link surfaceToolApprovalFrameAndRespond} 的臂相关性
|
|
912
|
+
* 门):条数必须等于所选 batch 的成员数。基数抓得到「少报/多报」,**抓不到**「条数对但内容是
|
|
913
|
+
* 另一批规则」—— 因为回执上没有任何锚能把这 N 条与那只 offer 对上(文本等式在这一位上不成立:
|
|
914
|
+
* 落盘的是**规范化后**的文本,与 offer 上的文本按设计可以不同,拿文本去比就是装第二个判官)。
|
|
915
|
+
* 🔴 ⇒ **锚缺席时端的呈现口径**:把它渲成「**引擎报告**落盘的规则」,**不是**「你刚才选的那批规则」。
|
|
916
|
+
* 两句话在正常情形下指同一件事,但只有前一句是本包在无锚时能背书的。
|
|
917
|
+
* **锚在场时**这条限制解除:`offerIndex` 已经把这份清单钉在用户真选的那只 offer 上。
|
|
902
918
|
*/
|
|
903
919
|
persistedRules?: readonly string[];
|
|
920
|
+
/**
|
|
921
|
+
* server ≥7.48.0(P-39 正位解;#340/#334 批臂回执的**归属锚**)——批臂兑付成功时,每条落地规则
|
|
922
|
+
* 与它**出自哪只 offer 的哪个成员**的对应关系。
|
|
923
|
+
*
|
|
924
|
+
* 直证(engine 7.48.0 fixture `@sema-agent/server/dist/tool-approval.js`):park 迟到腿 `:1360` 与
|
|
925
|
+
* live 腿 `:1461` 两处逐字铸
|
|
926
|
+
* `persistedRuleAnchors: persisted.members.map(m => ({ offerIndex: persistRule.batchOfferIndex,
|
|
927
|
+
* memberIndex: m.memberIndex, rule: m.rule }))`。
|
|
928
|
+
* 🔴 **载体是 respond 的 200 体**(与 `rulePersisted`/`persistedRules` 同级),**不是帧** ——
|
|
929
|
+
* 所以它进的是本 ack 视图,不进 {@link TOOL_APPROVAL_FRAME_KEYS_MIRROR}。
|
|
930
|
+
* 🔴 **只在批臂 ∧ 兑付成功 ∧ server 交得出 `members` 时在场**;单臂/失败/老 server ⇒ 缺席
|
|
931
|
+
* (**缺席 ≠ 空数组**:空数组会被读成「一条都没落」)。
|
|
932
|
+
* 🔴 **整只判形**(与 `persistedRules` 同族):坏形一律整只降缺席 —— 半份归属表比没有更坏,
|
|
933
|
+
* 它驱动的断言是「这条规则出自**你选的那只批**」。判形见 {@link readPersistedRuleAnchors}。
|
|
934
|
+
* 🔴 它**不替代** `persistedRules`:后者是展示序的规范文本清单(端逐条回显的那一份),
|
|
935
|
+
* 本位是给**相关性门**用的归属证据。两位同场时条数必须一致,否则本位整只降缺席
|
|
936
|
+
* (additive 位绝不回头削弱既有位的现行为)。
|
|
937
|
+
*/
|
|
938
|
+
persistedRuleAnchors?: readonly PersistedRuleAnchor[];
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* {@link ToolApprovalRespondAckView.persistedRuleAnchors} 的成员形(server 7.48.0 逐字三键)。
|
|
942
|
+
*
|
|
943
|
+
* `offerIndex` = 兑付时发出去的 `persistRule.batchOfferIndex`(整表同值 —— server 的 map 闭包捕获的
|
|
944
|
+
* 就是那一个值);`memberIndex` = 该条在 batch offer 成员表里的下标;`rule` = **落盘后**的规范文本
|
|
945
|
+
* (与 `persistedRules` 的同序成员同值;规范化是引擎的活,本包不复判文本)。
|
|
946
|
+
*/
|
|
947
|
+
export interface PersistedRuleAnchor {
|
|
948
|
+
readonly offerIndex: number;
|
|
949
|
+
readonly memberIndex: number;
|
|
950
|
+
readonly rule: string;
|
|
904
951
|
}
|
|
905
952
|
/**
|
|
906
953
|
* respond 回执的**结构化读口**(wire 是 JSON:注入面可能是旧 liveClient 的 raw fetch,也可能是
|
|
@@ -295,6 +295,12 @@ parkGatedCallId) {
|
|
|
295
295
|
...(pending.governanceForced === true ? { governanceForced: true } : {}),
|
|
296
296
|
...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
|
|
297
297
|
...(isWireRecordCarrier(durableProbeCause) ? { probeCause: durableProbeCause } : {}),
|
|
298
|
+
// #348(0.44.0)durable 腿的对偶:行上**本来就存着**这个值(server `parked-decide.ts`;SDK
|
|
299
|
+
// `PendingCheckpoint.toolCallId?: string | null` 直证)。与活卡帧腿同一个卡位、同一条缺席纪律 ——
|
|
300
|
+
// `null`(SDK 声明的第二种缺席形)与空串一并降键缺席,绝不把 `null` 折成串。
|
|
301
|
+
...(typeof pending.toolCallId === 'string' && pending.toolCallId !== ''
|
|
302
|
+
? { toolCallId: pending.toolCallId }
|
|
303
|
+
: {}),
|
|
298
304
|
});
|
|
299
305
|
switch (card.kind) {
|
|
300
306
|
case 'failed':
|
|
@@ -540,6 +546,11 @@ export function readToolApprovalRespondAck(v) {
|
|
|
540
546
|
// 🔴 **整只判形**(不逐条丢):合取批的回执是一份清单,漏掉一条就是对人少报了一次授权 ——
|
|
541
547
|
// 非数组 / 空 / 含非串或空串成员 ⇒ 整只降缺席(诚实「不知道」优先于半份清单)。
|
|
542
548
|
...(isNonEmptyStringArray(o.persistedRules) ? { persistedRules: o.persistedRules } : {}),
|
|
549
|
+
// P-39 归属锚(server ≥7.48.0):同样**整只判形**,理由见该位注。
|
|
550
|
+
...(() => {
|
|
551
|
+
const anchors = readPersistedRuleAnchors(o.persistedRuleAnchors);
|
|
552
|
+
return anchors !== undefined ? { persistedRuleAnchors: anchors } : {};
|
|
553
|
+
})(),
|
|
543
554
|
};
|
|
544
555
|
// 🔴 **两位规范文本回显的「同份 ack 内矛盾形」**(异源对抗复审 [high] 采纳,0.43.0)——
|
|
545
556
|
// 这与本函数不做的**相关性**判定(「这个 ack 是不是这一次审批的回执」,归编排层)是两件事:
|
|
@@ -551,13 +562,35 @@ export function readToolApprovalRespondAck(v) {
|
|
|
551
562
|
// 而 `rulePersisted` 那一位同时在说没存上。
|
|
552
563
|
// 两形都**只丢这两位**(不牵连整份 ack):`rememberApplied`/`updatedInputForwarded` 是更重的
|
|
553
564
|
// 安全告知位,不该被一条规则回显的坏形连坐掉。
|
|
554
|
-
|
|
565
|
+
// 🔴 归属锚随它所归属的那份清单一起判:单数回显是**编辑臂**的产物,批臂的归属锚与它同场 =
|
|
566
|
+
// 这份回执在说两次不同的授权(与下面①同一条理由,只是多一位要一起丢)。
|
|
567
|
+
if (out.persistedRule !== undefined && (out.persistedRules !== undefined || out.persistedRuleAnchors !== undefined)) {
|
|
555
568
|
delete out.persistedRule;
|
|
556
569
|
delete out.persistedRules;
|
|
570
|
+
delete out.persistedRuleAnchors;
|
|
557
571
|
}
|
|
558
|
-
else if ((out.persistedRule !== undefined || out.persistedRules !== undefined
|
|
572
|
+
else if ((out.persistedRule !== undefined || out.persistedRules !== undefined || out.persistedRuleAnchors !== undefined) &&
|
|
573
|
+
out.rulePersisted !== true) {
|
|
559
574
|
delete out.persistedRule;
|
|
560
575
|
delete out.persistedRules;
|
|
576
|
+
delete out.persistedRuleAnchors;
|
|
577
|
+
}
|
|
578
|
+
// 🔴 **跨位自洽**(P-39,0.44.0):锚是 `persistedRules` 的逐成员分解 —— server 两处铸点都在同一个
|
|
579
|
+
// `persisted` 结果上取 `rules` 与 `members`,所以条数恒等。不等 = 这份回执对「到底存了什么」
|
|
580
|
+
// 自相矛盾。**只丢锚、保留 `persistedRules`**:additive 位到货绝不回头削弱既有位的现行为
|
|
581
|
+
// (老 server 上本来就没有锚,那条路仍按基数门走)。
|
|
582
|
+
// 🔴 **逐位置同文本**(异源对抗复审二轮 [high] 采纳):server 侧两位是**同一个数组**的两种投影 ——
|
|
583
|
+
// dist `rules-consent.js` 逐字 `{ rules: anchored.map(a => a.rule), members: anchored }`,且两者
|
|
584
|
+
// 同按 `memberIndex` 排序 ⇒ `persistedRules[i] === anchors[i].rule` 是**铸造期保证**,不等只可能
|
|
585
|
+
// 来自被改过/串了台的回执。
|
|
586
|
+
// ⚠️ 与本文件「**不复判文本**」那条纪律不冲突:那条禁的是拿回执文本去比 **offer 上的**文本
|
|
587
|
+
// (落盘的是规范化后的字节,与 offer 按设计可以不同 —— 比它就是装第二个判官);这里比的是
|
|
588
|
+
// **同一份回执内部**两个位互相说的话,属「这个对象自己说不通」的形的范畴。
|
|
589
|
+
if (out.persistedRuleAnchors !== undefined &&
|
|
590
|
+
(out.persistedRules === undefined ||
|
|
591
|
+
out.persistedRules.length !== out.persistedRuleAnchors.length ||
|
|
592
|
+
out.persistedRuleAnchors.some((a, i) => a.rule !== out.persistedRules?.[i]))) {
|
|
593
|
+
delete out.persistedRuleAnchors;
|
|
561
594
|
}
|
|
562
595
|
return out;
|
|
563
596
|
}
|
|
@@ -565,6 +598,61 @@ export function readToolApprovalRespondAck(v) {
|
|
|
565
598
|
function isNonEmptyStringArray(v) {
|
|
566
599
|
return Array.isArray(v) && v.length > 0 && v.every(x => typeof x === 'string' && x !== '');
|
|
567
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* {@link ToolApprovalRespondAckView.persistedRuleAnchors} 的**整只**判形(P-39,server ≥7.48.0)。
|
|
603
|
+
*
|
|
604
|
+
* 逐条要求三键齐、`offerIndex`/`memberIndex` 是**非负安全整数**、`rule` 非空串;再加两条**同只回执内
|
|
605
|
+
* 自洽**:
|
|
606
|
+
* · `offerIndex` **整表同值** —— server 的 map 闭包捕获的是同一个 `persistRule.batchOfferIndex`
|
|
607
|
+
* (dist 直证),混合值的表不可能由合规 server 铸出;
|
|
608
|
+
* · `memberIndex` **不重复** —— 两条锚认领同一个成员,那份归属表就不是它自称的逐成员分解。
|
|
609
|
+
* 任一条不满足 ⇒ **整只降缺席**(不逐条丢):半份归属表会让相关性门拿着残表放行,比没有锚更坏。
|
|
610
|
+
*
|
|
611
|
+
* 🔴 下标**绝不折算**(不 `|0`、不 `Math.trunc`):一个被折过的下标指向的是**另一条** offer /
|
|
612
|
+
* 另一个成员,那正是本位要证伪的东西。
|
|
613
|
+
*/
|
|
614
|
+
function readPersistedRuleAnchors(v) {
|
|
615
|
+
if (!Array.isArray(v) || v.length === 0)
|
|
616
|
+
return undefined;
|
|
617
|
+
const out = [];
|
|
618
|
+
const seenMembers = new Set();
|
|
619
|
+
let firstOfferIndex;
|
|
620
|
+
for (const raw of v) {
|
|
621
|
+
if (raw === null || typeof raw !== 'object')
|
|
622
|
+
return undefined;
|
|
623
|
+
const o = raw;
|
|
624
|
+
const { offerIndex, memberIndex, rule } = o;
|
|
625
|
+
if (!isNonNegativeSafeInt(offerIndex) || !isNonNegativeSafeInt(memberIndex))
|
|
626
|
+
return undefined;
|
|
627
|
+
if (typeof rule !== 'string' || rule === '')
|
|
628
|
+
return undefined;
|
|
629
|
+
if (firstOfferIndex === undefined)
|
|
630
|
+
firstOfferIndex = offerIndex;
|
|
631
|
+
else if (offerIndex !== firstOfferIndex)
|
|
632
|
+
return undefined;
|
|
633
|
+
if (seenMembers.has(memberIndex))
|
|
634
|
+
return undefined;
|
|
635
|
+
seenMembers.add(memberIndex);
|
|
636
|
+
out.push({ offerIndex, memberIndex, rule });
|
|
637
|
+
}
|
|
638
|
+
// 🔴 **座位必须恰好铺满 `0..n-1`**(异源对抗复审二轮 [high] 采纳)。这不是本包发明的严格,是
|
|
639
|
+
// **server 自己的不变量**(dist `rules-consent.js` 的 `anchorBatchMembers`,逐字):
|
|
640
|
+
// · `anchored.length !== displayed.rules.length || distinctSeats !== anchored.length`
|
|
641
|
+
// ⇒ **整个不发 members**(锚要么是与人看见的那只 offer **一一对应的满覆盖**,要么缺席);
|
|
642
|
+
// · 发之前还 `anchored.sort((a,b) => a.memberIndex - b.memberIndex)`。
|
|
643
|
+
// ⇒ 合规锚表的 memberIndex 集合恒 = `{0..n-1}`。越界座位是 server 铸不出的形。
|
|
644
|
+
// ⚠️ 只校**集合**不校**顺序**:排序是 server 今天的实现细节,而集合是它成文的不变量
|
|
645
|
+
// ([verdict-must-accept-stronger-form]:别把一个合法的更强形判成回归)。
|
|
646
|
+
// ⚠️ 「7.48 引擎 + 锚缺席」是**合法形**(上面两条 warn 分支就是它)⇒ 缺席绝不可读作可疑。
|
|
647
|
+
for (const seat of seenMembers)
|
|
648
|
+
if (seat >= out.length)
|
|
649
|
+
return undefined;
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
/** 非负安全整数(下标位共用;`Number.isSafeInteger` 已排除 NaN/Infinity/小数)。 */
|
|
653
|
+
function isNonNegativeSafeInt(v) {
|
|
654
|
+
return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
|
|
655
|
+
}
|
|
568
656
|
/**
|
|
569
657
|
* 从 respond 抛出来的东西上读 {@link ToolApprovalRespondRefusal}。**永不抛、永不造**。
|
|
570
658
|
*
|
|
@@ -900,6 +988,12 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
900
988
|
args: args,
|
|
901
989
|
// A-028.3:live 帧腿的卡身份键经 gateIdentity 唯一铸口(帧上无 gatedCallId,以 approvalId 铸)。
|
|
902
990
|
callKey: liveFrameCallKey(frame.approvalId),
|
|
991
|
+
// #348(0.44.0):帧上的 core tool-call id 透传到卡口 —— 卡与 transcript 里 `tool_use` 块之间唯一的
|
|
992
|
+
// wire 事实锚。**非空串才 stamp**(空串是坏值不是「没有调用」,同文件既有窄读同族纪律);
|
|
993
|
+
// 缺席 ⇒ 键缺席,宿主按缺席降级(绝不拿 approvalId 冒充,那正是 #348 的病形)。
|
|
994
|
+
...(typeof frame.toolCallId === 'string' && frame.toolCallId !== ''
|
|
995
|
+
? { toolCallId: frame.toolCallId }
|
|
996
|
+
: {}),
|
|
903
997
|
...(signal ? { signal } : {}),
|
|
904
998
|
...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
|
|
905
999
|
...(wireNote !== undefined ? { wireNote } : {}),
|
|
@@ -1123,7 +1217,8 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
1123
1217
|
// 🔴 **只丢这两位、不牵连整份 ack**(与上面那道 id/decision 相关性门刻意不同姿势):那道门管的是
|
|
1124
1218
|
// 「整份回执根本不是我的」,这道门管的是「回执是我的,但其中一格与我发出的臂矛盾」——
|
|
1125
1219
|
// 整份丢会把 `rememberApplied`/`updatedInputForwarded` 这两个更重的安全告知位一起连坐掉。
|
|
1126
|
-
if (ack !== undefined &&
|
|
1220
|
+
if (ack !== undefined &&
|
|
1221
|
+
(ack.persistedRule !== undefined || ack.persistedRules !== undefined || ack.persistedRuleAnchors !== undefined)) {
|
|
1127
1222
|
// 🔴 单数回显**只属编辑臂**(异源对抗复审二轮 [medium] 采纳,server `editedArmEcho` 逐字:
|
|
1128
1223
|
// `persistRule.kind !== 'text' || !persistRule.edited || …` ⇒ 返 `{}`)——候选臂成功时
|
|
1129
1224
|
// server **不铸**这一位,所以候选臂上收到它同样是「一次没发生过的授权」。
|
|
@@ -1135,27 +1230,64 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
1135
1230
|
// ⚠️ 只做**基数**相关性,**不复判文本**:规范化是引擎的活(`Bash(adb *)` → `Bash(adb:*)`),
|
|
1136
1231
|
// 在这里比对文本就是装第二个判官。
|
|
1137
1232
|
const expectedBatchCount = batchArmTarget?.rules.length;
|
|
1233
|
+
// 🔴 **P-39 归属锚门**(server ≥7.48.0,0.44.0):锚在场时,相关性从「基数」升级到「身份」——
|
|
1234
|
+
// `offerIndex` 必须等于**这一次真发出去**的 `persistRuleBatchOfferIndex`,`memberIndex` 必须
|
|
1235
|
+
// 落在所选 offer 的成员表范围内。这正是 P-39 登记里基数门**抓不到**的那一形:
|
|
1236
|
+
// 「条数对,但这份回执说的是另一只 offer 的规则」。
|
|
1237
|
+
// ⚠️ 锚**缺席**(≤7.47 引擎)⇒ 本门整条让位,回落既有基数门 —— additive 位不许把老引擎判红。
|
|
1238
|
+
// 🔴 **absent / valid / invalid 必须是三态,不是两态**(异源对抗复审 [high] 采纳,本批):
|
|
1239
|
+
// 读器把「在场但坏形」也降成 `undefined`,若这里只看 `ack.persistedRuleAnchors === undefined`
|
|
1240
|
+
// 就把它与「老引擎真没这个键」混成一格 ⇒ 让位基数门。于是**「另一只 offer + 规则条数恰好相同
|
|
1241
|
+
// + 故意发一份坏锚」这条回执可以精确绕过本门刚加的身份校验** —— 攻击面正是本门要封的那一形。
|
|
1242
|
+
// ⇒ 判据取**wire 上键在不在**(raw),不取窄化产物在不在:
|
|
1243
|
+
// · 键真缺席 ⇒ 让位基数门(老引擎兼容);
|
|
1244
|
+
// · 键在场 ∧ 合形 ⇒ 走身份门;
|
|
1245
|
+
// · 键在场 ∧ 坏形 ⇒ **不许让位**,连同 `persistedRules` 一起丢(一份自称带归属却给不出
|
|
1246
|
+
// 合法归属的回执,不配驱动任何「已存了什么」的强断言)。
|
|
1247
|
+
// (结构视图读:SDK 的 respond 返回型上没有这个 additive 键,与同文件 `pending.ruleOffers`
|
|
1248
|
+
// / `riskDescriptor.probeCause` 同款姿势 —— 不引入新的宽 cast。)
|
|
1249
|
+
// 🔴 判据是**键在不在**(`hasOwnProperty`),**不是**「值是不是 `undefined`」(异源对抗复审
|
|
1250
|
+
// 二轮 [high] 采纳):注入面是**宿主给的 JS 函数**,不是 `JSON.parse` 的产物 —— 一个自铸
|
|
1251
|
+
// `{persistedRuleAnchors: undefined}` 的回执在 JS 里**键是在场的**,用值判会把它读成
|
|
1252
|
+
// 「老引擎真没这个键」而让位基数门,恰好绕过身份门。同仓 `engineCapState` 用 `in` 而不用
|
|
1253
|
+
// `!== undefined` 的成文理由与此同源(那处头注:「本表也接宿主注入的对象」)。
|
|
1254
|
+
const anchorKeyOnWire = typeof raw === 'object' &&
|
|
1255
|
+
raw !== null &&
|
|
1256
|
+
Object.prototype.hasOwnProperty.call(raw, 'persistedRuleAnchors');
|
|
1257
|
+
const anchors = ack.persistedRuleAnchors;
|
|
1258
|
+
const anchorsOk = anchors !== undefined
|
|
1259
|
+
? persistRuleBatchOfferIndex !== undefined &&
|
|
1260
|
+
expectedBatchCount !== undefined &&
|
|
1261
|
+
anchors.every(a => a.offerIndex === persistRuleBatchOfferIndex && a.memberIndex < expectedBatchCount)
|
|
1262
|
+
: !anchorKeyOnWire;
|
|
1138
1263
|
const batchEchoOk = ack.persistedRules !== undefined &&
|
|
1139
1264
|
expectedBatchCount !== undefined &&
|
|
1140
|
-
ack.persistedRules.length === expectedBatchCount
|
|
1265
|
+
ack.persistedRules.length === expectedBatchCount &&
|
|
1266
|
+
anchorsOk;
|
|
1141
1267
|
const strayEchoes = [];
|
|
1142
1268
|
if (ack.persistedRule !== undefined && !sentEditedArm)
|
|
1143
1269
|
strayEchoes.push('persistedRule');
|
|
1144
1270
|
if (ack.persistedRules !== undefined && !batchEchoOk)
|
|
1145
1271
|
strayEchoes.push('persistedRules');
|
|
1272
|
+
if (anchors !== undefined && !(anchorsOk && batchEchoOk))
|
|
1273
|
+
strayEchoes.push('persistedRuleAnchors');
|
|
1146
1274
|
if (strayEchoes.length > 0) {
|
|
1147
1275
|
hostLog('error', `liveToolApprovalWire: DISCARDING persisted-rule echo(es) [${strayEchoes.join(', ')}] on the ack for ` +
|
|
1148
1276
|
`${frame.approvalId} — they do not correlate with the persistence arm actually sent ` +
|
|
1149
1277
|
`(editedArm=${String(sentEditedArm)} batchArmMembers=${String(expectedBatchCount ?? 'none')} ` +
|
|
1150
|
-
`
|
|
1278
|
+
`sentOfferIndex=${String(persistRuleBatchOfferIndex ?? 'none')} ` +
|
|
1279
|
+
`echoedRules=${String(ack.persistedRules?.length ?? 'none')} ` +
|
|
1280
|
+
`echoedAnchorOffer=${String(anchors?.[0]?.offerIndex ?? 'none')}); never telling the user a rule ` +
|
|
1151
1281
|
'was stored off a receipt that does not line up with what this client actually sent');
|
|
1152
|
-
const { persistedRule: _pr, persistedRules: _prs, ...rest } = ack;
|
|
1282
|
+
const { persistedRule: _pr, persistedRules: _prs, persistedRuleAnchors: _pra, ...rest } = ack;
|
|
1153
1283
|
void _pr;
|
|
1154
1284
|
void _prs;
|
|
1285
|
+
void _pra;
|
|
1155
1286
|
ack = {
|
|
1156
1287
|
...rest,
|
|
1157
1288
|
...(ack.persistedRule !== undefined && sentEditedArm ? { persistedRule: ack.persistedRule } : {}),
|
|
1158
1289
|
...(ack.persistedRules !== undefined && batchEchoOk ? { persistedRules: ack.persistedRules } : {}),
|
|
1290
|
+
...(anchors !== undefined && anchorsOk && batchEchoOk ? { persistedRuleAnchors: anchors } : {}),
|
|
1159
1291
|
};
|
|
1160
1292
|
}
|
|
1161
1293
|
}
|
package/dist/hooksWireCaps.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HookFailureNotice } from './notifications.js';
|
|
1
2
|
import type { WireHooksConfig } from './finalVerifyWire.js';
|
|
2
3
|
export { GOAL_STOP_HOOK_WIRE_ENV, CC_STOP_SEMANTICS_MIN_SERVER, ccStopSemanticsFromVersion, engineCcStopSemantics, isGoalStopHookWireArmed, setWireSessionStopHook, getWireSessionStopHook, getWireSessionStopHookCcSemantics, buildGoalStopHookPrompt, } from './goalStopHook.js';
|
|
3
4
|
export type { WireHooksConfig };
|
|
@@ -8,3 +9,39 @@ export type { WireHooksConfig };
|
|
|
8
9
|
export declare function hooksForWire(): WireHooksConfig | undefined;
|
|
9
10
|
/** 测试钩:清 `/goal` 锁存(壳里靠进程边界隔离;包内同进程多组断言必须能清)。 */
|
|
10
11
|
export declare function __resetHooksWireCapsForTests(): void;
|
|
12
|
+
/**
|
|
13
|
+
* 这台引擎会不会发 `hook_non_blocking_failure` 观测帧(hook 自己坏了的那一族)。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **能力位嵌在 `fleet` 对象里**,不是顶层键 —— server `dist/http/routes/capabilities.js:52`:
|
|
16
|
+
* `fleet: { stream, sessionScope, observe, resume, bgNotifyFailClosed, steerPriority,
|
|
17
|
+
* hookFailureNotice: true }`。拿平铺口 `engineCapTrue(base,'hookFailureNotice')` 去读**恒 false**
|
|
18
|
+
* (白读),而白读的后果不是报错、是安静地把新车道判成没有。
|
|
19
|
+
* 🔴 未判/父键缺席/父键为 `false`(整族不供)/子键缺席 ⇒ **false** = fail-closed。
|
|
20
|
+
*
|
|
21
|
+
* 🔴 **它是 affordance 判据,不是上屏闸**:一帧真到货的故障通知**不该**被本位闸掉 —— 帧在手里
|
|
22
|
+
* 就是最强证据,而能力位在探测落地前恒 false,拿它闸会把早到的真实故障静默掉(方向与 #281 相反)。
|
|
23
|
+
* 本位的正当用途 = 「要不要在设置/状态面声称有这项监测」。判定口见 `notifications.ts`
|
|
24
|
+
* 的 `classifyHookFailureFrame`(那里刻意不读本位)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function fleetHookFailureNoticeCapable(baseUrl: string | undefined): boolean;
|
|
27
|
+
/** footer 横幅文案前缀(测试锁字面;与 `HOOK_NOTICE_WARN_PREFIX` 同族风格)。 */
|
|
28
|
+
export declare const HOOK_FAILURE_WARN_PREFIX = "Hook error";
|
|
29
|
+
/**
|
|
30
|
+
* 「这个 hook 自己坏了」的**单行**横幅文案(hook 名 + 故障形/退出码 + stderr 摘要)。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **为什么在本文件而不是 `notifications.ts`**:文案口要过展示消毒单源 `collapseLabel`
|
|
33
|
+
* (在 `fleetTaskDesc.ts`),而 `notifications.ts` 在 **A 层闭包**内(portability 门只降棘轮)——
|
|
34
|
+
* 从那里 import 会让闭包涨一格,手抄第二份字符集又是 `fleetTaskDesc` 顶注点名禁止的形。
|
|
35
|
+
* ⇒ 判定留 `notifications.ts`(它只交出**原文**),展示归本层。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 **绝不编造退出码**:`timeout` / `spawn_failed` 上没有 `exitCode`,那两形只报故障词
|
|
38
|
+
* (写 `exit 0` 会让人去查一个根本不存在的退出码)。
|
|
39
|
+
* 🔴 **两位都是 UNTRUSTED-for-display**(异源对抗复审二轮 [medium] 采纳):`hookName` 的一手源是
|
|
40
|
+
* **用户 settings 里自己写的** `statusMessage` / command / url / prompt;`stderr` 是**坏 hook 自己吐的
|
|
41
|
+
* 流**。server 只做脱敏 + 截断、**不做控制符消毒**(与 `workerBadge.name` / fleet 行标签同一条分工)。
|
|
42
|
+
* 不消毒的后果不是难看:一个带 `\n` / ESC / RLO 的 hook 名能把「**哪个**安全 hook 失效了」这条
|
|
43
|
+
* **唯一**告警重排、截断或整段藏掉,而这条横幅存在的全部理由就是让那件事看得见。
|
|
44
|
+
* 🔴 **单行**:渲染面是 `wrap="truncate"` 的一行;`collapseLabel` 同时做 `\s+` 折平与控制符/双向符/
|
|
45
|
+
* 孤代理项可见化转义,两件事一把做完(不在这里手抄第二份字符集)。
|
|
46
|
+
*/
|
|
47
|
+
export declare function hookFailureNoticeText(n: HookFailureNotice): string;
|
package/dist/hooksWireCaps.js
CHANGED
|
@@ -62,7 +62,10 @@
|
|
|
62
62
|
* and never inherit engine secrets; user-defined variables must ride `settings.env` — nothing for the
|
|
63
63
|
* shell to scrub here (commands are the user's own text).
|
|
64
64
|
*/
|
|
65
|
+
import { engineCapNestedTrue } from './engineCapsCache.js';
|
|
66
|
+
import { collapseLabel } from './fleetTaskDesc.js';
|
|
65
67
|
import { hostLog, hostSettings } from './host.js';
|
|
68
|
+
import { MAX_HOOK_NOTICE_TEXT_CHARS } from './notifications.js';
|
|
66
69
|
// REF-CC-156(split):`/goal` 版本判档 + prompt 组装 + 模块状态搬到 goalStopHook.ts。
|
|
67
70
|
// `goalStopHookMatcher`/`__resetGoalStopHookForTests` 是两文件间的私有接口(不 re-export);
|
|
68
71
|
// 其余七个名字原样 re-export,外部 import 路径(index 的 `export *`)零改动。
|
|
@@ -164,3 +167,49 @@ export function hooksForWire() {
|
|
|
164
167
|
export function __resetHooksWireCapsForTests() {
|
|
165
168
|
__resetGoalStopHookForTests();
|
|
166
169
|
}
|
|
170
|
+
// ── hook 观测面的能力位(#281 / server ≥7.48.0)──────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* 这台引擎会不会发 `hook_non_blocking_failure` 观测帧(hook 自己坏了的那一族)。
|
|
173
|
+
*
|
|
174
|
+
* 🔴 **能力位嵌在 `fleet` 对象里**,不是顶层键 —— server `dist/http/routes/capabilities.js:52`:
|
|
175
|
+
* `fleet: { stream, sessionScope, observe, resume, bgNotifyFailClosed, steerPriority,
|
|
176
|
+
* hookFailureNotice: true }`。拿平铺口 `engineCapTrue(base,'hookFailureNotice')` 去读**恒 false**
|
|
177
|
+
* (白读),而白读的后果不是报错、是安静地把新车道判成没有。
|
|
178
|
+
* 🔴 未判/父键缺席/父键为 `false`(整族不供)/子键缺席 ⇒ **false** = fail-closed。
|
|
179
|
+
*
|
|
180
|
+
* 🔴 **它是 affordance 判据,不是上屏闸**:一帧真到货的故障通知**不该**被本位闸掉 —— 帧在手里
|
|
181
|
+
* 就是最强证据,而能力位在探测落地前恒 false,拿它闸会把早到的真实故障静默掉(方向与 #281 相反)。
|
|
182
|
+
* 本位的正当用途 = 「要不要在设置/状态面声称有这项监测」。判定口见 `notifications.ts`
|
|
183
|
+
* 的 `classifyHookFailureFrame`(那里刻意不读本位)。
|
|
184
|
+
*/
|
|
185
|
+
export function fleetHookFailureNoticeCapable(baseUrl) {
|
|
186
|
+
return engineCapNestedTrue(baseUrl, 'fleet', 'hookFailureNotice');
|
|
187
|
+
}
|
|
188
|
+
/** footer 横幅文案前缀(测试锁字面;与 `HOOK_NOTICE_WARN_PREFIX` 同族风格)。 */
|
|
189
|
+
export const HOOK_FAILURE_WARN_PREFIX = 'Hook error';
|
|
190
|
+
/**
|
|
191
|
+
* 「这个 hook 自己坏了」的**单行**横幅文案(hook 名 + 故障形/退出码 + stderr 摘要)。
|
|
192
|
+
*
|
|
193
|
+
* 🔴 **为什么在本文件而不是 `notifications.ts`**:文案口要过展示消毒单源 `collapseLabel`
|
|
194
|
+
* (在 `fleetTaskDesc.ts`),而 `notifications.ts` 在 **A 层闭包**内(portability 门只降棘轮)——
|
|
195
|
+
* 从那里 import 会让闭包涨一格,手抄第二份字符集又是 `fleetTaskDesc` 顶注点名禁止的形。
|
|
196
|
+
* ⇒ 判定留 `notifications.ts`(它只交出**原文**),展示归本层。
|
|
197
|
+
*
|
|
198
|
+
* 🔴 **绝不编造退出码**:`timeout` / `spawn_failed` 上没有 `exitCode`,那两形只报故障词
|
|
199
|
+
* (写 `exit 0` 会让人去查一个根本不存在的退出码)。
|
|
200
|
+
* 🔴 **两位都是 UNTRUSTED-for-display**(异源对抗复审二轮 [medium] 采纳):`hookName` 的一手源是
|
|
201
|
+
* **用户 settings 里自己写的** `statusMessage` / command / url / prompt;`stderr` 是**坏 hook 自己吐的
|
|
202
|
+
* 流**。server 只做脱敏 + 截断、**不做控制符消毒**(与 `workerBadge.name` / fleet 行标签同一条分工)。
|
|
203
|
+
* 不消毒的后果不是难看:一个带 `\n` / ESC / RLO 的 hook 名能把「**哪个**安全 hook 失效了」这条
|
|
204
|
+
* **唯一**告警重排、截断或整段藏掉,而这条横幅存在的全部理由就是让那件事看得见。
|
|
205
|
+
* 🔴 **单行**:渲染面是 `wrap="truncate"` 的一行;`collapseLabel` 同时做 `\s+` 折平与控制符/双向符/
|
|
206
|
+
* 孤代理项可见化转义,两件事一把做完(不在这里手抄第二份字符集)。
|
|
207
|
+
*/
|
|
208
|
+
export function hookFailureNoticeText(n) {
|
|
209
|
+
const code = n.exitCode !== undefined ? `exit ${n.exitCode}` : n.reason;
|
|
210
|
+
const name = collapseLabel(n.hookName);
|
|
211
|
+
const tail = n.stderr !== undefined ? `: ${collapseLabel(n.stderr)}` : '';
|
|
212
|
+
const line = `${HOOK_FAILURE_WARN_PREFIX}: ${name} (${code})${tail}`;
|
|
213
|
+
// 有界兜底(与铸点同上限;消毒在前、截断在后 —— 反过来会把一个转义序列切成半截)。
|
|
214
|
+
return line.length <= MAX_HOOK_NOTICE_TEXT_CHARS ? line : line.slice(0, MAX_HOOK_NOTICE_TEXT_CHARS);
|
|
215
|
+
}
|
package/dist/notifications.d.ts
CHANGED
|
@@ -291,7 +291,13 @@ export declare function splitApiErrorSupplement(text: string): {
|
|
|
291
291
|
main: string;
|
|
292
292
|
supplement?: string;
|
|
293
293
|
};
|
|
294
|
-
/** SSE 平铺 wire 形(server 1.280.0 http/server.js:6347-6355 直证;ownerScope/ownerSessionId 上 wire 前剥掉)。
|
|
294
|
+
/** SSE 平铺 wire 形(server 1.280.0 http/server.js:6347-6355 直证;ownerScope/ownerSessionId 上 wire 前剥掉)。
|
|
295
|
+
* 🔴 平铺形在 7.48.0 仍逐字成立:`dist/http/routes/fleet.js:146` =
|
|
296
|
+
* `send("hook_notice", { type:"hook_notice", ...hookNoticeWire(hn), ts })`(spread,不是 `notice:` 嵌套)。
|
|
297
|
+
* 🔴 本形是**第一族**(`hook_decision_unavailable`)的平铺视图,**刻意不为第二族加宽**:
|
|
298
|
+
* 两族只共用 `type`/`kind`/`event`/`ts`,而 `reason` 的**值域各自闭集**(同名不同义)。
|
|
299
|
+
* 第二族的专有键走本文件内部视图 `HookFailureWireView`(不出包边界)—— 把两族的键混进同一个
|
|
300
|
+
* 导出形,等于对外承诺一个「哪一族都不完全对」的形,消费端读哪一位都得再猜一次 kind。 */
|
|
295
301
|
export interface HookNoticeWire {
|
|
296
302
|
type?: unknown;
|
|
297
303
|
kind?: unknown;
|
|
@@ -338,4 +344,59 @@ export declare function isHookNoticeFrame(frame: unknown): boolean;
|
|
|
338
344
|
export declare function hookNoticeLastCheckText(n: GoalHookNotice): string;
|
|
339
345
|
/** footer 通知条文案前缀(测试锁字面;宿主拼 ` (${reason})`)。 */
|
|
340
346
|
export declare const HOOK_NOTICE_WARN_PREFIX = "Goal check: couldn't evaluate this turn \u2014 proceeded";
|
|
347
|
+
/** 机器可判的故障形(dist 逐字闭集)。 */
|
|
348
|
+
export type HookFailureReason = 'exit_nonzero' | 'timeout' | 'spawn_failed' | 'bad_json';
|
|
349
|
+
/** 条目类型 —— 没有它,`hookName` 是命令还是 URL 还是提示词全靠猜(dist 逐字闭集)。 */
|
|
350
|
+
export type HookEntryType = 'command' | 'http' | 'prompt' | 'agent';
|
|
351
|
+
/** 宿主写 store / 渲横幅的形(与 {@link GoalHookNotice} 平级,但**不是**同一件事)。 */
|
|
352
|
+
export interface HookFailureNotice {
|
|
353
|
+
/** 哪个 hook 事件(`PreToolUse` / `Stop` / …)。开集:server 不闭这一位。 */
|
|
354
|
+
event: string;
|
|
355
|
+
reason: HookFailureReason;
|
|
356
|
+
/** hook 身份(server 侧 `statusMessage` 优先,否则 command / url / prompt),已脱敏 + 有界。 */
|
|
357
|
+
hookName: string;
|
|
358
|
+
entryType: HookEntryType;
|
|
359
|
+
/** 🔴 **超时 / spawn 失败没有退出码 ⇒ 缺席**(绝不铸 0 那种看着合法的假读数)。 */
|
|
360
|
+
exitCode?: number;
|
|
361
|
+
/** 工具事件才有(PreToolUse/PostToolUse/PostToolUseFailure);非工具事件缺席。 */
|
|
362
|
+
toolName?: string;
|
|
363
|
+
/** hook 的 stderr 摘要(server 已脱敏 + 截断;本包边界再兜底一次)。空 ⇒ 缺席。 */
|
|
364
|
+
stderr?: string;
|
|
365
|
+
detail?: string;
|
|
366
|
+
at: number;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 观测帧自由文本上限 —— 与 server `MAX_HOOK_NOTICE_TEXT_CHARS`(`fleet/fleet-bus.d.ts`)同值。
|
|
370
|
+
* 🔴 server 已截过一遍(那一遍拿得到原文,是**承重**的);本包这一遍是**边界兜底**,挡住忘了截断的
|
|
371
|
+
* 未来生产者把一条无界流灌进单行横幅。幂等:输出恒 ≤ 上限。
|
|
372
|
+
*/
|
|
373
|
+
export declare const MAX_HOOK_NOTICE_TEXT_CHARS = 512;
|
|
374
|
+
/** 第二族的判定结果。drop 原因供宿主打 SEMA_DEBUG 档。 */
|
|
375
|
+
export type HookFailureVerdict = {
|
|
376
|
+
ok: true;
|
|
377
|
+
failure: HookFailureNotice;
|
|
378
|
+
} | {
|
|
379
|
+
ok: false;
|
|
380
|
+
drop: 'not-hook-notice' | 'unknown-kind' | 'not-session-scoped' | 'unknown-reason' | 'unknown-entry-type' | 'no-hook-name';
|
|
381
|
+
kind?: string;
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* `hook_non_blocking_failure` 帧的判定本体(#281 的客户端半场)。
|
|
385
|
+
*
|
|
386
|
+
* 🔴 **与 {@link classifyHookNoticeFrame} 是两个口,不是一个口的两条臂**:两族的载荷形不同
|
|
387
|
+
* (那族出 {@link GoalHookNotice},本族出 {@link HookFailureNotice}),把它们塞进同一个返回联合会
|
|
388
|
+
* **破坏既有消费端**(现在 `v.ok` 之后直接读 `v.notice` 的写法要全部改窄化)。分口 ⇒ 姊妹口的行为
|
|
389
|
+
* **逐字节不变**(它对本族仍报 `unknown-kind`,常驻套对这一格有钉),宿主按 `kind` 分派一次即可。
|
|
390
|
+
*
|
|
391
|
+
* 🔴 **闭集位一律「不认就丢」,绝不折成某一档**:与姊妹族「表外 reason 归一到 `skipped`」刻意不同 ——
|
|
392
|
+
* 那一族的 `skipped` 有真兜底语义(「跳过了」),本族**没有**「兜底故障形」这种东西,把一个没见过的
|
|
393
|
+
* 词折成 `exit_nonzero` 就是替引擎编一个它没说过的故障。
|
|
394
|
+
*
|
|
395
|
+
* 🔴 `hookName` 缺席即丢:一条「某个 hook 坏了、但不知道是哪个」的横幅,用户拿它什么都做不了,
|
|
396
|
+
* 只会制造焦虑 —— 而 server 侧这一位是**必填**,缺席意味着这不是一帧合规的本族帧。
|
|
397
|
+
*/
|
|
398
|
+
export declare function classifyHookFailureFrame(frame: unknown, opts: {
|
|
399
|
+
sessionScoped: boolean;
|
|
400
|
+
now?: number;
|
|
401
|
+
}): HookFailureVerdict;
|
|
341
402
|
export {};
|
package/dist/notifications.js
CHANGED
|
@@ -1090,3 +1090,71 @@ export function hookNoticeLastCheckText(n) {
|
|
|
1090
1090
|
}
|
|
1091
1091
|
/** footer 通知条文案前缀(测试锁字面;宿主拼 ` (${reason})`)。 */
|
|
1092
1092
|
export const HOOK_NOTICE_WARN_PREFIX = 'Goal check: couldn\'t evaluate this turn — proceeded';
|
|
1093
|
+
const KNOWN_FAILURE_REASONS = new Set(['exit_nonzero', 'timeout', 'spawn_failed', 'bad_json']);
|
|
1094
|
+
const KNOWN_ENTRY_TYPES = new Set(['command', 'http', 'prompt', 'agent']);
|
|
1095
|
+
/**
|
|
1096
|
+
* 观测帧自由文本上限 —— 与 server `MAX_HOOK_NOTICE_TEXT_CHARS`(`fleet/fleet-bus.d.ts`)同值。
|
|
1097
|
+
* 🔴 server 已截过一遍(那一遍拿得到原文,是**承重**的);本包这一遍是**边界兜底**,挡住忘了截断的
|
|
1098
|
+
* 未来生产者把一条无界流灌进单行横幅。幂等:输出恒 ≤ 上限。
|
|
1099
|
+
*/
|
|
1100
|
+
export const MAX_HOOK_NOTICE_TEXT_CHARS = 512;
|
|
1101
|
+
function boundedNoticeText(s) {
|
|
1102
|
+
return s.length <= MAX_HOOK_NOTICE_TEXT_CHARS ? s : s.slice(0, MAX_HOOK_NOTICE_TEXT_CHARS);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* `hook_non_blocking_failure` 帧的判定本体(#281 的客户端半场)。
|
|
1106
|
+
*
|
|
1107
|
+
* 🔴 **与 {@link classifyHookNoticeFrame} 是两个口,不是一个口的两条臂**:两族的载荷形不同
|
|
1108
|
+
* (那族出 {@link GoalHookNotice},本族出 {@link HookFailureNotice}),把它们塞进同一个返回联合会
|
|
1109
|
+
* **破坏既有消费端**(现在 `v.ok` 之后直接读 `v.notice` 的写法要全部改窄化)。分口 ⇒ 姊妹口的行为
|
|
1110
|
+
* **逐字节不变**(它对本族仍报 `unknown-kind`,常驻套对这一格有钉),宿主按 `kind` 分派一次即可。
|
|
1111
|
+
*
|
|
1112
|
+
* 🔴 **闭集位一律「不认就丢」,绝不折成某一档**:与姊妹族「表外 reason 归一到 `skipped`」刻意不同 ——
|
|
1113
|
+
* 那一族的 `skipped` 有真兜底语义(「跳过了」),本族**没有**「兜底故障形」这种东西,把一个没见过的
|
|
1114
|
+
* 词折成 `exit_nonzero` 就是替引擎编一个它没说过的故障。
|
|
1115
|
+
*
|
|
1116
|
+
* 🔴 `hookName` 缺席即丢:一条「某个 hook 坏了、但不知道是哪个」的横幅,用户拿它什么都做不了,
|
|
1117
|
+
* 只会制造焦虑 —— 而 server 侧这一位是**必填**,缺席意味着这不是一帧合规的本族帧。
|
|
1118
|
+
*/
|
|
1119
|
+
export function classifyHookFailureFrame(frame, opts) {
|
|
1120
|
+
const f = frame;
|
|
1121
|
+
if (f?.type !== 'hook_notice')
|
|
1122
|
+
return { ok: false, drop: 'not-hook-notice' };
|
|
1123
|
+
if (f.kind !== 'hook_non_blocking_failure')
|
|
1124
|
+
return { ok: false, drop: 'unknown-kind', kind: String(f.kind) };
|
|
1125
|
+
// 🔴 归属镜像(与姊妹族同一条 fail-CLOSED):server 只对 scoped 连接过滤。
|
|
1126
|
+
if (!opts.sessionScoped)
|
|
1127
|
+
return { ok: false, drop: 'not-session-scoped' };
|
|
1128
|
+
if (typeof f.reason !== 'string' || !KNOWN_FAILURE_REASONS.has(f.reason))
|
|
1129
|
+
return { ok: false, drop: 'unknown-reason' };
|
|
1130
|
+
if (typeof f.entryType !== 'string' || !KNOWN_ENTRY_TYPES.has(f.entryType))
|
|
1131
|
+
return { ok: false, drop: 'unknown-entry-type' };
|
|
1132
|
+
if (typeof f.hookName !== 'string' || f.hookName === '')
|
|
1133
|
+
return { ok: false, drop: 'no-hook-name' };
|
|
1134
|
+
return {
|
|
1135
|
+
ok: true,
|
|
1136
|
+
failure: {
|
|
1137
|
+
event: typeof f.event === 'string' ? f.event : '',
|
|
1138
|
+
reason: f.reason,
|
|
1139
|
+
hookName: boundedNoticeText(f.hookName),
|
|
1140
|
+
entryType: f.entryType,
|
|
1141
|
+
// 🔴 非整数/非数一律降缺席 —— 退出码是给人照着查的,一个 1.5 或 'x' 比没有更坏。
|
|
1142
|
+
...(typeof f.exitCode === 'number' && Number.isSafeInteger(f.exitCode) ? { exitCode: f.exitCode } : {}),
|
|
1143
|
+
...(typeof f.toolName === 'string' && f.toolName ? { toolName: boundedNoticeText(f.toolName) } : {}),
|
|
1144
|
+
...(typeof f.stderr === 'string' && f.stderr ? { stderr: boundedNoticeText(f.stderr) } : {}),
|
|
1145
|
+
...(typeof f.detail === 'string' && f.detail ? { detail: boundedNoticeText(f.detail) } : {}),
|
|
1146
|
+
at: typeof f.ts === 'number' ? f.ts : (opts.now ?? Date.now()),
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
// 🔴 本族的**横幅文案口** `hookFailureNoticeText` / `HOOK_FAILURE_WARN_PREFIX` 与能力位读口一样,
|
|
1151
|
+
// 刻意**不在本文件**,而在 `hooksWireCaps.ts` —— 理由同 `fleetHookFailureNoticeCapable`:本文件在
|
|
1152
|
+
// **A 层闭包**内(portability 门 ①b,只降棘轮),而文案口必须过展示消毒单源 `collapseLabel`
|
|
1153
|
+
// (在 `fleetTaskDesc.ts`),把那个文件拉进闭包会让棘轮当场涨一格;在本文件手抄第二份字符集
|
|
1154
|
+
// 则正是 `fleetTaskDesc` 顶注点名禁止的形(「改一处必须改另一处」)。
|
|
1155
|
+
// ⇒ **判定留本文件、展示归那一层**,与包内既有分工一致(本文件只交出**原文** `hookName`/`stderr`,
|
|
1156
|
+
// UNTRUSTED-for-display 的消毒责任在渲染侧)。
|
|
1157
|
+
// 🔴 本族的**能力位读口** `fleetHookFailureNoticeCapable` 刻意**不放在本文件**,而在
|
|
1158
|
+
// `hooksWireCaps.ts` —— 本文件在 **A 层闭包**内(portability 门 ①b,只降棘轮),而能力位要读
|
|
1159
|
+
// `engineCapsCache`,把那个文件拉进闭包会让棘轮当场涨一格。能力位本就属 `*WireCaps` 那一层
|
|
1160
|
+
// (与 `hooksForWire` 同域),放那里是归位不是绕道。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| peer:wire 契约 | `@sema-agent/sdk` **>=7.2.0**(value-level,非 type-only) | `package.json` `peerDependencies` |
|
|
24
24
|
| peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
|
|
25
25
|
| runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
|
|
26
|
-
| 公开导出面 | **
|
|
26
|
+
| 公开导出面 | **783** 个运行期符号(+ 40 个测试钩;= 未发 server 7.48.0 消费批的值,npm `0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
|
|
27
27
|
| 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
|
|
28
28
|
| 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
|
|
29
29
|
|
|
@@ -101,7 +101,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
101
101
|
|
|
102
102
|
## §2 公共导出面地图(按域)
|
|
103
103
|
|
|
104
|
-
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**
|
|
104
|
+
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**783** 项)。
|
|
105
105
|
> 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
|
|
106
106
|
> **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
|
|
107
107
|
> 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
|
|
@@ -111,7 +111,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
111
111
|
|
|
112
112
|
`public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
|
|
113
113
|
`scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
|
|
114
|
-
实测:
|
|
114
|
+
实测:783 项 **100% 是运行期导出,零 type-only**。
|
|
115
115
|
|
|
116
116
|
**推论(端必须知道)**:
|
|
117
117
|
- barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
|
|
@@ -120,30 +120,30 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
120
120
|
端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
|
|
121
121
|
- `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
783 项的内部构成(帮助端估读表大小):**232** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
|
|
124
124
|
(矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
|
|
125
125
|
(`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
|
|
126
126
|
**39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
|
|
127
127
|
|
|
128
|
-
### 2b. 域图(16 域,逐域计数之和 =
|
|
128
|
+
### 2b. 域图(16 域,逐域计数之和 = 783)
|
|
129
129
|
|
|
130
130
|
| # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
|
|
131
131
|
|---|---|---|---|---|---|
|
|
132
132
|
| 1 | **适配内核(下行主链)** | 33 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
|
|
133
133
|
| 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
|
|
134
|
-
| 3 | **HITL 决断卡链**(§4/§5 主战场) |
|
|
134
|
+
| 3 | **HITL 决断卡链**(§4/§5 主战场) | 131 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
|
|
135
135
|
| 4 | **子代 wire + 面板侧信道台账** | 82 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent`(0.32.0 未发布 #280 件A:additive 第三参 `childTaskId` —— 端有行上下文时**应当**传,传了就走「台账优先 / 缺席即诚实缺席 + `noteBgOwnerAbsence` 留痕」的 Q3 口径,与 tail·taskOutput·subagentOutput 三腿同姿势、与孪生 resume 腿共用同一个 `resolveOwnerRunId` 判据;**不传**则逐字维持旧行为=回落在飞 run)· `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `clearBgTerminalFacts`(#242 批 3 扫码修:复活=新周期,旧周期终态事实作废——fleetLedger 复活两腿按尾段清账,factsAccepted 方向核不再拿上周期终态当先例)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
|
|
136
136
|
| 5 | **fleet 投影** | 46 | `createFleetLedger` · `projectTasks` · `projectWorkflows` · `projectFleetAgentRows` · `readEngineActiveBgTasks` · `FLEET_TASK_VIEW_KEYS` · `escapeDisplayControlChars`(不可见字符可见化,行标签/描述消毒的共享底座)· `wireCycleSeq` / `wireRetiredBy`(0.38.0 提货补投的 #261 §2 两位:代际号 = SendMessage 复活即 +1,**缺席 ≠ 第一代**;`retiredBy` 在场 = 这条终态是对账腿从 durable run 行投影出来的、**不是**发布方亲报 —— 幽灵行与正常收尾唯一的 wire 判据。两位都只在场才落键) | 老 `fleetClient` 那一刀的成品:**帧体归库、连接归端** —— 端持 SSE 连接,库做行投影 + 保留台账 | `src/fleet/fleetProjection.ts`、`src/fleet/fleetLedger.ts`、`src/fleetAgentPanelProjection.ts`、`src/fleetTaskDesc.ts` |
|
|
137
137
|
| 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
|
|
138
|
-
| 7 | **通知与 outstanding 台账** |
|
|
138
|
+
| 7 | **通知与 outstanding 台账** | 41 | `installNotificationQueuePort` · `normalizeTaskNotification` · `taskNotificationDedupKeyFromWire` · `registerOutstandingBgTask` / `registerOutstandingWorkflowRun` · `notificationQueuePortMisses` · `subscribeOutstandingWorkflows` · `outstandingDeliverableWorkflowCount` | `task_notification` 归一 + 去重 + 投递进宿主命令队列的**一把闸**;`outstandingDeliverableWorkflowCount()` 是 headless `-p` 的**退出门** | `src/notifications.ts`(11 个 module 台账) |
|
|
139
139
|
| 8 | **工具结果卡** | 25 | `structuredToToolUseResult` · `readAsyncLaunchedAgentReceipt` · `wireOutputToBody` · `parseModelFacingBash` · `getPatchFromContents` · `toolEndResultToUserFrame` · `flattenToolOutput` | 铸端要渲的 `tool_result` 卡体,含客户端 diff hunk(唯一 runtime dep 的用处) | `src/toolResult.ts`、`src/printToolResultFrame.ts`、`src/diff/patch.ts` |
|
|
140
|
-
| 9 | **能力/旋钮 wire 门族** |
|
|
140
|
+
| 9 | **能力/旋钮 wire 门族** | 87 | `projectAgentsForWire` / `awaitTaskAgentsWire` · `hooksForWire` · `mcpConfigsToSpecs` · `ultracodeForRequest` · `resolveWebSearch` + `buildWebSearchEnv` · `attachmentsForRequest` · `projectEffectiveBody` | 逐功能的「引擎有没有这个能力 + 这个字段怎么上 wire」投影器,由 `buildTaskRequest` 组装。🔴 `hooksForWire()` 是 **fail-closed**:无 `SettingsPort` ⇒ 返回 `undefined`(信任门,不是取值) | 17 个 `src/*WireCaps.ts` + `src/scratchpadWireCaps.ts`、`src/hooksWireCaps.ts`、`src/goalStopHook.ts`、`src/websearch/searchProviderPresets.ts` |
|
|
141
141
|
| 10 | **headless / 部署旋钮 wire** | 61 | `parseSandboxArgv` / `sandboxRequestFields` · `parseLimitsArgv` / `limitsForPrint` · `resolveHeadlessFinalVerify` · `resolveHeadlessPermissionMode` · `resolveHeadlessInteractiveTools` · `armDetachCancel` + `detachCancelArm` + `isDetachArmed` · `withHeadlessR1Reconnect` | `-p`/headless 车道的 env+argv 旋钮。🔴 `detachWire` 是**拆**的补偿:判定与 cancel-arm 台账在库,信号路径的裸 fetch 留宿主(`detachCancelArm()` 是取件口) | `src/sandboxWire.ts`、`scenarioWire.ts`、`finalVerifyWire.ts`、`limitsWire.ts`、`interactiveToolsWire.ts`、`headlessPermissionModeWire.ts`、`headlessReconnectWire.ts`、`detachWire.ts` |
|
|
142
142
|
| 11 | **模型目录与预算** | 67 | `resolveModelCatalog` · `loadCatalogWithSources` · `PROVIDER_PRESETS` / `MODEL_FAMILIES` · `defaultMaxTokensFor` · `getLiveModelCatalog` / `setLiveModelCatalogRefresher` · `providerAuthMethods` / `beginDeviceCodeAuth` · `providerCatalogRows` / `providerCatalogRowDetail` / `providerPresetById`(#244 F4 族D A-028.17:46 家表的规范折表层 —— 全表不重排、诚实缺席「model id typed in」,cli 目录/web 向导同一份折表)· `TIER_ORDER` / `CC_TIER_ALIASES` / `isTier` / `resolveTierBinding`(A-028.18:档位词表+校验+fail-open 降档派生单源;settings 存储归宿主)· `resolveEntryVision` / `computeDeleteBlockers` / `computeDeleteWarnings`(0.38.0 #318 件③ 上收:Model Hub 供给面的三端公共判定 —— vision 生效值+来源三态、删除断链核(拒删+指路)、删除降级后果(照删但必说)。**零 IO**,读盘那半场留各端;`doc === null` 的两义在调用方分流) | 三层 provider 目录解析(线上 URL → 包内预设 → 用户覆盖)+ per-model `maxTokens` 封顶。线上腿需注入 `CatalogFetchJson`,缺席 ⇒ 整条不启用(`online.reason='no-fetch-port'`);缓存落盘经 `CatalogCachePort` | `src/model/{catalog,catalogLoader,providerAuth,providerPresets,providerCatalog,tierVocabulary}.ts`、`src/liveModelCatalog.ts`、`src/modelBudgetRule.ts`、`src/sessionModelLatch.ts`、`src/effortWire.ts` |
|
|
143
143
|
| 12 | **workflow 与后台工作视图** | 19 | `projectWorkflowRun` · `createLiveWorkflowSource` · `ensureWorkflowActivityLedger` · `readWorkflowActivityLedger` · `stopWorkflowActivityLedger` · `resetWorkflowActivityLedgers` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
|
|
144
144
|
| 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词)。🔴 **证据等级标注(0.42.0,test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件)**:该文件里所有以「CC 如何如何」为形的断言(`212 methods` / `854-channel census` / 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标)**证据等级 = 桌面 unpack,本地语料库不可复验** —— 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,拿它去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不证真也不证伪**。复核这些断言**不得**拿本仓语料当反证 |
|
|
145
145
|
| 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
|
|
146
|
-
| 15 | **控制面与传输** |
|
|
146
|
+
| 15 | **控制面与传输** | 74 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` / `invalidateEngineCaps(baseUrl, probe?)`(#307 S25:引擎温切后的 caps 生产失效口 —— kick 自带幂等闸,同 baseUrl 重启后不显式失效就永远读到旧引擎那一版能力位;调用方 = 壳的 respawn/restartEngine。🔴 **推荐两参形**:第二参给替代探测则「推进代际 + 注册新探测」在同一同步块内完成,失效与下一次 kick 之间那个「等待者读到未判」的窗按构造不存在;单参形保留给「只丢缓存、这一刻没有替代探测」的调用方,那种情形下读到未判是诚实结局) · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `clearRunningChoiceOffer`(Inkglow-1085 P0b①:「Do nothing」登记的清口 —— 端的「重新打开操作菜单」入口;登记在场时 attemptActiveRunSelfHeal 不整卡重弹,not-parked 结局带 `alreadyOffered: true` 判别位,端据此降级渲一行)· `INTERACTIVE_WAY_OUT`(默认出路串单源)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
|
|
147
147
|
| 16 | **引擎词汇表与包自检** | 48 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` · `DELEGATION_CAP_CODES` / `isDelegationCapCode` / `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP`(0.38.0 #318 件④:core 5.48.0 design/323 委派席位到限**两码,处置不对称禁合并** —— 并发帽=**可等**(兄弟结束即有位)/ 会话累计帽=**等也没用**(这条会话的配额用尽))· `CONFIG_DELEGATION_ENTRY_CAPS`(同批入 `CONFIG_REFUSAL_CODES` 识别表)· `delegationCapDispositionOf` / `MCP_SERVER_REVOKED`(0.39.0 载体到货消费件:core 5.50.0 补 `{ error: code, code }` 孪生拼法后两码真上 `tool_end.errorCode`,0.38.0「先立词不落消费分支」的已知局限自此解除;处置轴 `wait-for-slot` / `reuse-existing-or-await-reap` 机器可读(累计帽=retained-window 帐,行回收配额即回,处置=SendMessage 复用,**非**「换会话/永久耗尽」——0.38.0 段该句系勘误),未知 `delegation.*` 码 ⇒ `undefined`;`mcp.server_revoked` = 操作员 mid-session 吊销 server 后的工具面本地闸(被吊销的 server **名**今天不过 wire 境:detail.server 是进程内位,抬升腿只 lift code——归因渲染候 core 补 typed detail,已点名);载体门 = engine-vocab G3 腿锚 core dist 铸点) | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(计数以 `scripts/public-export-baseline.json` 为准,别手抄;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`;#318 件④ 补 `delegation.*` 族四位 + `config.delegation_entry_caps`;0.39.0 补三新码消费件三位)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
|
|
148
148
|
|
|
149
149
|
🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
|
|
@@ -1164,7 +1164,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
|
|
|
1164
1164
|
| **P-33** | low | **`CANCEL_DENY_BUDGET_MS` 的论证前提已作废,数值未动**(0.42.0 §2.4 撤稿件的如实残余):那 2s 预算的原理由是「DENY-abort 语义上引擎收到即终结 run」,而 server `ASSISTANT-WIRE-CONTRACT.md` §4a 逐字反对(**DENY 是 TOOL 级应答,永远不是 run kill**)。于是「decide 慢」与「deny 丢了」这两件事在 2s 这个刻度上**不可分**,晚到的成功也不会撤回那行 warn(只有 10s 自清) | `src/hitl/hitlHostSurface.ts`(`CANCEL_DENY_BUDGET_MS` 头注的重审段 + `observeCancelByDeny`) | 端**不要**把 `CANCEL_DENY_WARN_TEXT` 那行当成「会话一定锁死了」的判据——它今天只证明「2s 内没收到 settle」。根治要做成两档(软档只记 debug、硬档才上屏),那是**跨仓一批**(包侧改时序 + 壳侧同批换判据与用例),本批**刻意不做单边改动** |
|
|
1165
1165
|
| **P-35** | low | **`WIRE_NETWORK_ERROR_PATTERN` 与 web 的 `NETWORK_PATTERNS` 存在真实的**大小写敏感度分叉**(0.42.0 [C195] 行为对拍腿实测):本包整条基表带 `i` flag,而 web 的 errno 类 token(`ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `EAI_AGAIN` / `ENOTFOUND` / `EHOSTUNREACH` / `ENETUNREACH` / `EPIPE` / `UND_ERR`)**逐条无 `i`** ⇒ 同一条小写 errno 文本,本包判 `transport`、web 判 `unknown` | `src/wireErrorTriage.ts`(`WIRE_NETWORK_ERROR_PATTERN`);对拍腿 = `scripts/run-client-core-pure-test.mjs` 的 `F3E-C195-web-parity` 条件腿 | 分叉**已登记并被门钉住**(登记表在对拍腿里,分叉消失即红、未登记的新分歧也红)。端今天照现状读即可;要收敛得两侧同批改 flags —— 这属跨仓一批,已按表态制上 C 板 |
|
|
1166
1166
|
| **P-40** | med(能力位模型;**系统性、非本批引入**——`approvalDecisionNote`/`respondFreeFormRules` 自 0.29.0/0.42.0 起同形) @cli @web @desktop | 🆕 **能力位是 per-baseUrl 的布尔缓存,不与「出这条帧的副本」或「收 respond 的副本」绑定**(异源对抗复审六轮 [medium] 登记)。`engineCapTrue(baseUrl, key)` 答的是「这个地址上次探到什么」——引擎温切 / 滚动升级 / 多副本代理下,一个 `true` 可以来自 7.46 实例而 respond 落到 ≤7.45 实例。后果按位不同:`note` 被静默忽略(无害);**编辑臂/批臂**会被老解析器**响亮 400 且连决断一起拒** ⇒ 人按下的 allow 落不了地,退化成 `unresolved`/TTL 自决。⚠️ ⇒ 「能力位闸让老引擎**零受迫**」这句话的正确形是「**在能力读数与实际 responder 一致的前提下**零受迫」,本档 §4b-2/§4b-3 的措辞已按此订正 | `src/engineCapsCache.ts`(per-baseUrl 缓存 + `invalidateEngineCaps(baseUrl, probe?)`)· `src/hitl/toolApprovalWire.ts`(`ToolApprovalFrameLaneOpts` 三个能力位的闸) | 端的现有缓解:①引擎 respawn/restart 后**必须**调 `invalidateEngineCaps(baseUrl, probe?)`(推荐两参形:推进代际 + 注册新探测在同一同步块内完成);②滚动升级窗口内宁可把两个规则能力位按 `false` 供给(丢持久臂 + 一条诚实通知,远好于把决断打掉)。🔴 正位解需要**上游给出 per-responder 的能力证明**(回执/帧上带副本代际或集群最低能力),或让 server 保证这类 400 对决断**零副作用**(那样客户端可以去掉批臂重送原决断)—— 按接入文档宪法向 server 点名,属跨仓设计件 |
|
|
1167
|
-
| **P-39** |
|
|
1167
|
+
| **P-39** | ✅ **已解**(server 7.48.0 到货,client-core 本批消费) @cli @web @desktop | **上游已补 additive 归属锚 `persistedRuleAnchors`** —— 兑付回执(respond 200 体,**非帧**)上每条落地规则带 `{offerIndex, memberIndex, rule}`(直证:engine 7.48.0 fixture `@sema-agent/server/dist/tool-approval.js:1360` park 迟到腿 / `:1461` live 腿两处铸点)。本包据它把批臂相关性从**基数**升级到**身份**:锚的 `offerIndex` 必须等于这一次真发出去的 `persistRuleBatchOfferIndex`、`memberIndex` 必须落在所选 offer 的成员范围内 ⇒ 原登记里「条数对但内容是另一批规则」那一形**从此抓得到**。🔴 **三态不是两态**(本批对抗复审 [high] 采纳):「键真缺席」(≤7.47 老引擎)才让位基数门;**「键在场但坏形」绝不让位** —— 否则一份「另一只 offer + 条数恰好相同 + 故意发坏锚」的回执可精确绕过身份门。判据取 **wire 上键在不在**,不取窄化产物在不在 | `src/hitl/toolApprovalWire.ts`(`ToolApprovalRespondAckView.persistedRuleAnchors` + `readPersistedRuleAnchors` 整只判形 + `surfaceToolApprovalFrameAndRespond` 臂相关性门的 absent/valid/invalid 三态)· 行为钉:`run-hitl-gate-honesty-test.mjs` F14 件1(正控/另一只 offer 负控/memberIndex 越界负控/老引擎兼容/**坏形绕过反测**)、`run-rules-side-test.mjs` G8(读口整只判形十格) | 端**可以**渲「你选的那批里的 N 条」了 —— 但**只在锚到位且核得住时**;锚缺席(老引擎)仍须回落原口径「**引擎报告**落盘的规则」,绝不声称「就是你选的那批」。cli 半场参照实现 = `persistedRulesWire.ts` `persistedNoticeText`(三合取:注入面交了 `batchOfferIndex` ∧ 锚过判形 ∧ `offerIndex` 对得上,任一不满足即退回保守句) |
|
|
1168
1168
|
| **P-38** | med(集成面;**存量、非本批引入** —— 0.42.0 基线 `4c364c1` 上逐字相同) @cli @web @desktop | 🆕 **标准帧路由丢弃 `ToolApprovalFrameOutcome` 的诊断/回显面**:`routeToolApprovalFrame`(`src/hitl/frameRouter.ts`)对 `surfaceToolApprovalFrameAndRespond` 的产物**只解构 `decision`**,`ack` 与 `respondRefusal` 整段丢掉;`AskGateWireDeps` 上也没有 outcome sink。⇒ 走**标准集成面**(`runStream` → frameRouter)的端读不到:① 0.42.0 #225 件5 的 `respondRefusal`(server 响亮 400 / `edit-rejected` 拒句)② 0.43.0 #334 的 `persistedRule` / `persistedRules` 规范文本回显。⚠️ 两个**更重**的安全位不在此列 —— `rememberApplied:false` / `updatedInputForwarded:false` 由 `toolApprovalWire` 就地经 `HitlHostSurface` 投递(`surfaceRememberNotApplied` / `surfaceEditNotForwarded`),**不经**本路由,所以那两条告知照常到达。⚠️ 端若**自己直调** `surfaceToolApprovalFrameAndRespond`(cli 的 `approvalStreamWire` 卡腿即是)则拿得到完整 outcome,本条只约束走 frameRouter 那条路 | `src/hitl/frameRouter.ts`(`const { decision } = await surfaceToolApprovalFrameAndRespond(...)`)· `src/hitl/toolApprovalWire.ts`(`ToolApprovalFrameOutcome` 的三位产物) | 端**不要**假定「包返回了这一位 ⇒ 标准路由上就能读到」:走 frameRouter 的端今天拿不到规则回显与拒句,渲「你的规则已存为 X」/「引擎说:…」必须先确认自己走的是哪条调用点。🔴 正位解在包侧(给 frameRouter / `AskGateWireDeps` 补一个 outcome 回执端口,或把规则回显与拒句经 `HitlHostSurface` 就地投递,与 `rememberApplied` 同姿势),属**行为面 + 三端接线**的独立设计件,按 [C162] 令④ 回 C 板提 |
|
|
1169
1169
|
| **P-37** | low(镜像面) @cli @web @desktop | 🆕 **`tool_approval_complete.parked` 已进键镜像,但本包零消费**(0.43.0 同形族扫的产物,#329 server 7.44.0 及更早就在场)。它是 `outcome:"expired"` 一词三义(park / 当场 deny / 无设施 deny)里 **park 那一义的显式判别位**:在场 ⇔ 这条 ask 按 park 路由收尾且墓碑已落(同一把 `approvalId` 仍可走迟到受理)。本包今天只把它镜像进 `TOOL_APPROVAL_FRAME_KEYS_MIRROR` + 类型,**没有**任何消费面 —— 因为它只长在 `tool_approval_complete` 上,而那种帧根本不进卡口(`surfaceToolApprovalFrameAndRespond` 消费的是 `tool_approval`) | `src/hitl/toolApprovalWire.ts`(`ToolApprovalFrame.parked` 声明 + 镜像项,两处 JSDoc 写明理由)· `scripts/run-approval-frame-keys-test.mjs` 的 `AHEAD_OF_ANCHOR` 登记 | 端今天**不要**指望包把「卡失效」改渲成「已转后台候批」——那条投影还没有。🔴 **缺席禁读作「真 deny」**:无店部署 / deny 政策 / 被连坐 VOID 的兄弟都发不出这个键,缺席只是「无 park 证据」。要把它变成一条真投影是**行为面**改动(消费者 = 端的完成帧处理面),按 [C162] 令④ 回 C 板提 |
|
|
1170
1170
|
| **P-36** | low | **中断文案归一只覆盖 `Operation aborted` 这一串**(#323 症状②,0.43.0;clay 裁定的**明确边界**,不是漏做):同一次用户中断里,**执行前被连坐**的旁观者拿的是 core 的另一串 `operation aborted before execution`,`interrupted_never_started` 族又是第三种;这两族**刻意不并入**中断改写臂 —— core 显式拒绝合并两串向([4973]),两串各承真语义(`Operation aborted` = 执行中被中止 / 该串 = 从未执行),而且它们**各有自己的文案与折叠腿**(端侧的 interrupted-batch 折叠 + `TOOL_END_INTERRUPTED_CODES` 词表)。⇒ 纯取消批里,那两族的 tool_end 今天仍按各自原文呈现 | `src/hitl/frameRouter.ts`(`isUserInterruptRewritable` 的判据①头注 + `isEngineAbortToolEnd` 的两串族说明)· 负控 = `scripts/run-hitl-gate-honesty-test.mjs` F13-d | 端**不要**假定「用户中断 ⇒ 这一批 tool_end 文案全是 CC 中断串」;两族按各自既有腿归因(机读码优先,文案兜底)。要不要并成一形是**语义裁定**不是实现细节,需 clay 先裁 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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",
|