@sema-agent/client-core 0.39.0 → 0.41.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 +299 -0
- package/README.md +1 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +16 -0
- package/dist/classifierVerdictWire.d.ts +0 -25
- package/dist/classifierVerdictWire.js +27 -7
- package/dist/engineAgentPanelStore.js +102 -13
- package/dist/hitl/approvalsFeed.d.ts +2 -2
- package/dist/hitl/approvalsFeed.js +57 -2
- package/dist/hitl/askGateWire.d.ts +1 -0
- package/dist/hitl/askGateWire.js +8 -0
- package/dist/hitl/frameRouter.d.ts +21 -2
- package/dist/hitl/frameRouter.js +144 -15
- package/dist/hitl/gateLedger.d.ts +95 -3
- package/dist/hitl/gateLedger.js +157 -3
- package/dist/hitl/hitlBridge.d.ts +53 -11
- package/dist/hitl/hitlBridge.js +97 -30
- package/dist/hitl/parkResolver.js +30 -5
- package/dist/hitl/toolApprovalWire.d.ts +15 -1
- package/dist/hitl/toolApprovalWire.js +20 -2
- package/dist/subagent/engineDelegatedPrompt.js +10 -3
- package/dist/systemReminderTag.d.ts +52 -0
- package/dist/systemReminderTag.js +73 -0
- package/docs/INTEGRATION-CLIENTS.md +100 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -28,6 +28,305 @@
|
|
|
28
28
|
> 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
|
|
29
29
|
> `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
|
|
30
30
|
|
|
31
|
+
## 0.41.0(2026-08-23)
|
|
32
|
+
|
|
33
|
+
**patch:七件真缺陷修(逃逸计数跨腿残留 / 面板缓冲无界 / 单源闸的撞名绕行口 / 决断成功摘错扣留帧 /
|
|
34
|
+
连坐 abort 帧的判据认不出旁观者 / `<system-reminder>` 带属性后两处判定恒失效)。**
|
|
35
|
+
各件互不相干,各自红先绿后、各自带常驻回归钉;公开面**零新增导出**(`GatePark.gatedCallId` 与
|
|
36
|
+
`GateLedger.dropHeldForDecidedPark` 都在既有类型上 additive;新叶件 `systemReminderTag.ts` 是包内
|
|
37
|
+
闸口,不进 barrel,消费者全在包内)。
|
|
38
|
+
|
|
39
|
+
- **🔴 [4974] 件⑥⑦(同根因双座位):core ≥5.46 的 `<system-reminder>` **开标签恒带 `mark`**,
|
|
40
|
+
两处判定还锚着裸标签**。真字节直证(core 5.52.0 `dist/core/reminder-mint.js`):
|
|
41
|
+
`openSystemReminder(mark)` = 有 mark 时铸 `<system-reminder mark="<22 位 base64url>">`,
|
|
42
|
+
`mintSystemReminder(body, mark)` = 开标签 + `\n` + 正文 + `\n</system-reminder>`(闭标签不变)。
|
|
43
|
+
两个座位:
|
|
44
|
+
· **件⑥ `classifierVerdictWire.unwrapSystemReminder`** —— 剥不掉壳 ⇒ 裁决原文前顶着整个开标签 ⇒
|
|
45
|
+
`startsWith(签名)` 恒不成立 ⇒ auto-deny 的通知行与 Recent Denials 记账**静默失效**(现网 core
|
|
46
|
+
全都带 mark,等于这条呈现面整条不在);
|
|
47
|
+
· **件⑦ `subagent/engineDelegatedPrompt.stripEnvelopes`** —— 信封整段剥不掉 ⇒ Fleet 详情页的
|
|
48
|
+
「Prompt」又变回 ~1330 字符技能清单(该函数头注自陈 2026-08-01 修过的 bug 原样复发)。
|
|
49
|
+
修:识别口径收进**单一闸口** `src/systemReminderTag.ts`(零 import 叶件)。开标签是**闭集**形
|
|
50
|
+
`<system-reminder(?: mark="[A-Za-z0-9_-]{22}")?>` —— core 只铸这两种(`openSystemReminder` 逐字,
|
|
51
|
+
mark 值形 = `MARK_SHAPE`),所以判据也只认这两种:**不做通配属性**(七审 finding② 采纳)。
|
|
52
|
+
宽口会把 `onclick="…"` / 换行垃圾 / 未闭合引号 / 内嵌 `<img` 都当成合法开标签,而这两个消费点
|
|
53
|
+
处理的正是**模型与工具产出的文本**(UNTRUSTED)—— 等于给伪造信封开口子:可以拿假信封把内容藏起来
|
|
54
|
+
(委派 prompt 面)或把自己的文本伪装成引擎裁决(分类器面)。闭标签逐字不放宽,同理。两处判定改经闸口 —— 两处各写一份正则正是这条缺陷能长出
|
|
55
|
+
**两个**座位的原因。`fork-boilerplate` 在 core dist 里是**裸**标签,保持精确形(没有属性的标签
|
|
56
|
+
不需要属性容忍)。🔴 闸口**刻意不校验 mark 值**:它的职责是「认出这是引擎的信封壳」并剥掉,
|
|
57
|
+
不是信任判定(core 的 `reminderMarkDeclaration` 明说带不带 mark 都可能是数据)——把安全判定藏进
|
|
58
|
+
一个显示层剥壳函数里,才是下一个 bug。
|
|
59
|
+
- **🔴 [4974] 候选③:分类器 deny 的三个铸点、三种前缀**(core 5.52.0 dist 逐字枚举):
|
|
60
|
+
① `hooks.js:606` 基础形;② `prepare-task.js:3103/3192` 继承自祖先层形;③ `prepare-task.js:3023`
|
|
61
|
+
「批准过的编辑」在祖先层被拦形。三者尾巴同形(`: reason` 或 `: [category]`)。
|
|
62
|
+
修前:② 被 ① 的前缀腿蒙对但 **reason 整条丢失**(tail 以 ` at an inherited…` 开头),③ **完全
|
|
63
|
+
不识别**。修:三形收成一张**逐字**表,**长的排前**(短的是长的前缀,顺序即语义),命中后按同一
|
|
64
|
+
条尾巴规则取 reason。逐字锚不放宽成模糊匹配(B8 ALLOW 登记的既有纪律,core 改文案必须跟车)。
|
|
65
|
+
🔴 **前缀不是语法**(七审 finding③ 采纳):命中之后还要看**边界** —— 铸点的尾巴只有两种(到此
|
|
66
|
+
为止,或紧跟 `': '`)。少了这条,`…ancestor layerX` 之类近似串也会被判成分类器裁决,而那条路径会
|
|
67
|
+
`markEnded` + 摘 HOLD + 写 Recent Denials + 用裁决原文覆盖渲染 —— 任何能产出工具错误正文的工具
|
|
68
|
+
都能伪造。
|
|
69
|
+
门:pure ㊲ 段(带 mark 剥壳 2 + 裸标签负控 1 + 伪造属性/坏 mark 形两条负控 2 + 三铸点矩阵 3 +
|
|
70
|
+
形近串负控 1 + 边界闸四条 4)/ B6 delegated 段(带 mark 信封剥壳 1 + 只有信封时留白 1 + 未闭合
|
|
71
|
+
保守不动 1 + 伪造属性负控 1),下限 B2 302→315 / B6 370→374。
|
|
72
|
+
|
|
73
|
+
- **🔴 [4970] 件⑤:连坐 abort 帧的判据认不出**旁观者**(0.40.0 机读位在现网零命中)**:
|
|
74
|
+
hold-poison 判据锚的是 `tool_end.output === "Operation aborted"` 精确串,而 core 的**两个短路点
|
|
75
|
+
产两种文案** —— gate 主角(abort 早退)= 精确串;**同批被连坐的旁观者**(执行前短路)=
|
|
76
|
+
`"operation aborted before execution"`(小写 + 后缀)。⇒ 判据对旁观者**恰好全不命中**:它们的
|
|
77
|
+
毒化帧根本没进 HOLD(当场上屏 + 被通用收口臂记 `markEnded`,resume 后重放的真结果被静默去重),
|
|
78
|
+
于是 0.40.0 那个 `_sema_collateral_abort` 机读位在**真实现网一次都没出现过**
|
|
79
|
+
(test 线真机取证:真 server 7.41.0 + 真 Postgres + client-core 0.40.0,1 Bash 待审批 +
|
|
80
|
+
2 并行 WebFetch,G1 零 stamp)。本文件旧注写的「core 对被 gate/**连坐** abort 的 call **恒**铸
|
|
81
|
+
这一串」对旁观者不成立,已按实证撤回。
|
|
82
|
+
修:判据改**双判据 OR**(单源 `isEngineAbortToolEnd`,`isGatedToolEnd` 与 hold-poison 臂两处
|
|
83
|
+
同批换)——①**主锚** `tool_end.errorCode === "gate.parked"`;②**兜底**=既有精确串。
|
|
84
|
+
🔴 **主锚是契约不是巧合**(core 官方答复 [4973]):两个 abort 短路点**消费同一个分类席**,而
|
|
85
|
+
park 提交**先于** abort 发射 ⇒ 铸点读那一席无竞态,主角与旁观者**恒**同席同码。(SDK 的
|
|
86
|
+
`Event_tool_end.errorCode` 声明处也逐字点名这个码;供给点见 core 5.52.0 dist `prepare-task` 的
|
|
87
|
+
`abortResultDetails: () => ({code:"gate.parked"})`。)
|
|
88
|
+
🔴 **兜底臂有现役覆盖面,不是遗留兼容**([4973]②):core 现役**唯一不铸**本码的臂 =
|
|
89
|
+
**post-hook signal re-check**(工具**已经执行完**、hook 之后签核那一拍才发现 abort,历来铸的
|
|
90
|
+
abort 结果不带 details ⇒ 无 errorCode)。少了兜底臂,这条臂整条落空 —— 双判据设计正为它而立。
|
|
91
|
+
🔴 两臂都**不放宽成模糊匹配**:主锚只认那一个**具名码**(「有 errorCode 就算」会把
|
|
92
|
+
`path_not_in_root` 这类普通工具错拖进 HOLD —— 正是 [2084]①-b 收窄掉的病),兜底不认文案家族。
|
|
93
|
+
两臂都不命中 = 没有「因 gate 中止」的任何证据 ⇒ 照旧普通收口(老 core + 变体串就落这一格,
|
|
94
|
+
诚实缺席)。
|
|
95
|
+
🔴 **上游不会去合并两条串**([4973],core 显式拒绝改串向 —— **不是**「core 待修」):两串各承
|
|
96
|
+
真语义(`"Operation aborted"` = 执行中被中止 / `"operation aborted before execution"` = 从未执行)。
|
|
97
|
+
🔴 **本判据只锚 errorCode、不锚任何 call id**([4973]③):plan-review / resource-slice 这类不持有
|
|
98
|
+
call 的 park 照样带本码但诚实缺席 id —— 顺手锚 id 就把那几族的连坐帧判成「认不出」。
|
|
99
|
+
🔴 **臂间掩蔽面同批堵住**(四审 finding① 采纳):放宽的是 HOLD 那条**超集**判据,而 ②`deny-stamp-next`
|
|
100
|
+
用的是一次性 + **位置性**的全局记号(「下一张 gated 报错帧就是那张被拒的门」)—— 超集谓词会让
|
|
101
|
+
同批连坐的旁观者**先到先吃**:它被改写成「用户拒绝了」并记 ended,真正被拒的 call 反倒只拿到引擎
|
|
102
|
+
原文([2393] hitl-F1 的用户面,方向还反了)。⇒ 那条臂改用**窄口**(名字腿 ∪ 确切串 = 件⑤ 之前
|
|
103
|
+
那一版逐字),新加的连坐码臂刻意不进来。
|
|
104
|
+
门:**F11-a**(G1 主形)/ **F11-b**(兜底臂 = post-hook re-check 形)/ **F11-c、F11-d**(双负控:
|
|
105
|
+
无码变体串、表外码普通错 —— 判据锚在帧的**位置**(零滞后上屏)而不只是文案,变异 M10 实撞)/
|
|
106
|
+
**F11-e**(旁观者先到也抢不走 deny 记号)。
|
|
107
|
+
|
|
108
|
+
- **🔴 [F4957-1] `approvalsFeed` 逃逸计数跨 push 重连残留**:`consecutivePollEscapes` 的唯一清零点
|
|
109
|
+
是「轮询跑完一整拍」,而 push 腿**重连**那条臂(`runStream` 的 `mode !== 'push'`)停掉轮询腿却
|
|
110
|
+
不动这个计数 ⇒ 被一整段**健康 push 窗**隔开的两批逃逸被当成「连续」累加。后果不是多算几次:
|
|
111
|
+
刚从 push 回落的轮询腿才逃逸两拍就撞上限(`MAX_CONSECUTIVE_POLL_ESCAPES=5`),`mode()` 谎报
|
|
112
|
+
idle —— 而文件头卖给端的自检正是「跑了 N 秒之后 mode 不是 idle」,它会指着一条**刚活过来**的腿
|
|
113
|
+
说它停摆了。修:重连一拍与「轮询跑完一整拍」是同等强度的健康证据(端真的从服务端收到了事件),
|
|
114
|
+
同批清零 —— 计数的语义是**连续**,就只能被真正连续的失败喂大。
|
|
115
|
+
🔴 **在途拍同治**(异源复审 finding③ 采纳):清零只管已经落地的计数,而一条**早于重连起跑**的
|
|
116
|
+
poll 可能还挂在 `await take()` 里,它稍后逃逸时会把计数从 0 又推回 1 —— 那一个单位是**上一条腿**
|
|
117
|
+
的失败,却活到了下一次回落之后(回落后只需 4 拍就转 idle)。修:轮询腿带**代际**(重连即翻篇),
|
|
118
|
+
非当代的完成回调既不改计数也不续排(续排归当代那条链)。
|
|
119
|
+
🔴 **代际闸排在提交之前**(二审 finding② 采纳):守卫若只挡「计数与续排」,`take()` 早就已经改完
|
|
120
|
+
`lastDigest`/`revision`/`last` 并通知了订阅者 —— 于是「旧 poll 在途 → push 重连并发布新快照 →
|
|
121
|
+
旧 poll 才带着**更老**的结果返回」这条乱序路径上,端会拿到一张 revision 更高、内容更旧的
|
|
122
|
+
`mode:'poll'` 快照(刚出现的待审批被抹掉,或刚解决的又冒回来)。闸前移到提交面。
|
|
123
|
+
🔴 **乱序闸盖住每一条取件路径**(三审 finding② 采纳):`refresh()`(端上「用户点了刷新」)走同一个
|
|
124
|
+
`take()` 却不带代际 ⇒ 同一条回滚路径原样重现。闸从「代际」升成**单调请求序号**(发号 + 提交前核
|
|
125
|
+
「有没有更晚发起的请求已经落地」),poll / push / refresh 三条路共用一把 —— 乱序是取件面的通病,
|
|
126
|
+
不是轮询腿的私事。
|
|
127
|
+
门:`run-hitl-gate-honesty-test.mjs` **F5-d / F5-e / F5-f / F5-g**(判据按 `list` **调用序号**编脚本、在途拍由
|
|
128
|
+
测试显式放行,逃逸次数是确定值不随机器负载漂;负控 = 既有 F5「真·5 连逃逸 ⇒ 诚实转 idle」与
|
|
129
|
+
F5-c「单次逃逸归零」原样绿)。
|
|
130
|
+
- **🔴 [F4957-2] `engineAgentPanelStore` 缓冲区无界**:满员臂只找 `tick` 驱逐,**找不到也照样
|
|
131
|
+
`push`** ⇒ 一个 tick 都没有的场景里 `MAX_BUFFER=200` 形同虚设。这不是理论形:`fleet-row` 是
|
|
132
|
+
后台子代在面板上的**唯一**在场 lane(它们的进度根本不走 `task_progress`),面板长期未挂载 +
|
|
133
|
+
后台子代活跃 = 宿主内存被 UNTRUSTED wire 事件按条吃(实测 500 条 fleet-row 全部留在表里)。
|
|
134
|
+
修:按**事件类别**而不是单一 kind 定规则 —— PROGRESS(`tick`/`fleet-row`,累计 rollup)按
|
|
135
|
+
(kind, taskId) 合并、满员时让位;LIFECYCLE(`end`/`sweep`)照旧必活(w0zwpa251 #4:被挤掉的
|
|
136
|
+
`end` 让面板行永远停在 running)。整表皆 LIFECYCLE 的极端形按 FIFO 顶掉最老一条 —— 有限缓冲下
|
|
137
|
+
只能保住「最新」,而每 turn 末必发的 `sweep`(语义 = 结掉所有还在跑的行)恰恰就是最新那条。
|
|
138
|
+
顺带:`fleet-row` 从此与 `tick` 同款按 taskId 合并(此前同一个后台子代每一拍都占一格)。
|
|
139
|
+
🔴 **lifecycle 是硬屏障,屏障两侧的 progress 一条都不合并**(二审 finding② + 六审 finding② 采纳):
|
|
140
|
+
就地覆盖会把 `progress(A,v1) → end(A) → progress(A,v2)` 重写成 `progress(A,v2) → end(A)`(复活进度
|
|
141
|
+
被搬到结算之前);而只把顺序修对(摘旧条、新条入队尾)仍然**吞掉屏障之前那一拍** ——
|
|
142
|
+
`tick(v1) → sweep → tick(v2)` 压成 `sweep → tick(v2)` 时,直发是「建行 → 结算 → 迟到 tick 被消费端
|
|
143
|
+
忽略」= 行留终态,回放却成了「对空表 sweep → v2 建出一条 **running** 行」= 幽灵在飞行。
|
|
144
|
+
判据是消费后的**终态等价**,不是事件数组长得像 ⇒ `sweep`(全局)与同 task 的 `end` 都是屏障。
|
|
145
|
+
屏障多占格子是对的取舍:上界仍由驱逐策略守着,而错折叠是**永久**静默错。
|
|
146
|
+
🔴 **合并必须逐键继承**(四审 finding③ 采纳):两条 progress lane 发的都是**部分帧**(投影层
|
|
147
|
+
`...(toolUses !== undefined ? { toolUses } : {})`),缺席的语义是「这一帧没说 ⇒ 保留行上已有值」,
|
|
148
|
+
粘性由消费端持。所以「后一条整只顶掉前一条」会把只在前一条出现过的键**永久丢掉**(面板一直挂着
|
|
149
|
+
时看得到 `toolUses`,回放却看不到)—— 本段自己立的「回放 ≡ 直发」不变量当场被破。合并改成
|
|
150
|
+
**逐键继承**(后到者赢,后到者缺席的键继承先到者),位置仍取最新那条。
|
|
151
|
+
🔴 **跨 lane 不合并**(五审 finding② 采纳):两条 progress lane 的消费规则不同 —— fleet-row 臂头注
|
|
152
|
+
写死「tick 一到就永久让位」。`fleet-row(v1) → tick → fleet-row(v2)` 若压成 `tick → merged`,直发时
|
|
153
|
+
**让位之前**落地的 v1(transcriptId/startedAt 这类只此一帧的位)在回放里永远到不了消费端。
|
|
154
|
+
⇒ 同 taskId 的**另一条 lane** 事件是合并屏障(倒序找合并目标,撞到对面 lane 即停)。
|
|
155
|
+
🔴 **相邻 sweep 去重**(二审 finding③ 采纳):sweep 自述幂等(结掉所有还在跑的行),两条之间什么
|
|
156
|
+
都没发生时第二条是严格 no-op。不去重的代价不是多一格 —— 面板长期未挂载时每 turn 一条 sweep 能把
|
|
157
|
+
200 格**全占满**,此后屏障之后的复活进度一律让位,那一行在回放里永远停在 completed。
|
|
158
|
+
门:`run-client-core-pure-test.mjs` 段 **⑧b**(有界 / 有界≠清空 / 合并 / 满员时 lifecycle 仍进得来
|
|
159
|
+
/ tick 合并不回归 / end 屏障与 sweep 屏障各一条时序钉 / 两条 lane 各一条逐键继承钉 / 跨 lane 屏障钉
|
|
160
|
+
/ 相邻 sweep 去重 + 全 lifecycle 满员后的复活进度仍进得来),下限 B1 225→238。
|
|
161
|
+
- **🔴 [F4947-1] `HitlBridge.findPending` —— 单源闸的撞名绕行口**(test 线 P0-KPI 复现件):
|
|
162
|
+
「哪一行 pending 是人正要决定的」这段查找,REF-CC-029 已把三份近似复刻收编成单一导出
|
|
163
|
+
`findPendingForTask`,P-30 又只给**那一个**加了门种闸(把 `plan_review`/`resource_limit` 挡在
|
|
164
|
+
「任意行」兜底之外)。但类里还留着一个**同名私有** `findPending`,而真实生产入口 ——
|
|
165
|
+
CC 兼容适配器 `makeHitlCanUseTool` → `decideTool(outcome, toolUseID)`,从不传
|
|
166
|
+
`preResolvedPending` —— 走的正是这条没有闸的老路:同 task 停着 plan_review 行时,它被当成
|
|
167
|
+
「这个 run 的 pending」交给工具审批 decide(空 toolName 卡 / 409 `gate_not_tool_approval`)。
|
|
168
|
+
修:私有件**删除并改调单源**(不补第二份过滤 —— 复刻正是这条缺陷的成因),名字一并换成
|
|
169
|
+
`pendingForDecide`(撞名正是「看起来只有一份实现」的成因);它那条**更强的身份腿**
|
|
170
|
+
(`toolUseID` 逐字命中)作为 `findPendingForTask` 的新 `callId` 参数带过去,同样只在过完门种闸
|
|
171
|
+
的行里找。🔴 失败形分层保住:`no_pending`(良性缺席)才回 null,`approvals.list` 真失败照旧抛
|
|
172
|
+
—— 那个码在 `parkResolver` 里的语义是「这张 gate 已被解决 ⇒ 继续读流」,把网络失败塞进去 =
|
|
173
|
+
谎报已解决(#110 烧会话那条路径)。
|
|
174
|
+
🔴 **身份腿要过族闸,且族闸读整行**(四审 finding② + 五审 finding① 采纳):门种闸只挡
|
|
175
|
+
plan_review/resource_limit 这类**外族**,AskUserQuestion 与工具审批**同在**可决集合里 —— 身份腿
|
|
176
|
+
若只比 taskId+callId,一个陈旧/错配/被复用的 park 身份就能让 fs 腿选中一行 ask(反之亦然),
|
|
177
|
+
那一行随后原样当 `preResolvedPending` 去呈卡 + decide。身份与本腿族属冲突 ⇒ **忽略该身份**、
|
|
178
|
+
照旧走族谓词回落(不动「身份缺席不 fail-closed」的政策:冲突是数据自相矛盾,不是查无此行)。
|
|
179
|
+
🔴 族闸必须**读整行**而不是只读 toolName:工具审批族有**两条腿**(一等 `kind==='tool_approval'`
|
|
180
|
+
放行任意 toolName + fs/shell 名字腿),而 wire 传下来的 `matches` 只有名字腿那半 —— 拿它当身份闸,
|
|
181
|
+
**合法的 kind-only 行**即使逐字命中也会被判出局,回落挑了同 task 的另一行(卡面与决断全落在别的
|
|
182
|
+
审批上、真 gate 继续 suspended)。⇒ 身份闸由调用方按自己那条腿的**完整**族判据供给
|
|
183
|
+
(fs 腿 = `isToolApprovalGate({kind: row.gateKind, toolName})`,ask 腿 = `isAskTool`)。
|
|
184
|
+
🔴 **身份腿不许跨 run**(二审 finding① 采纳):`/v1/approvals` 是 scope 级单队列,身份腿同样约束在
|
|
185
|
+
本 `taskId` 之内 —— 一个撞名/被复用的 callId 否则就能把**别人会话**的 pending 行交给这次 decide
|
|
186
|
+
(REF-CC-022 拆掉 `?? pending[0]` 防的正是这件事)。**刻意不 fail-closed**(该项部分驳回,理由
|
|
187
|
+
成文于参数头注):身份在场而队列没有那一行时照旧回落 —— 回落的失效面与本参数出现之前逐字节相同,
|
|
188
|
+
fail-closed 的失效面则是「每一次审批都决断不了」,而这个 join 本仓没有对真 server 的实证。
|
|
189
|
+
🔴 **族冲突 ⇒ 这一轮候选池整体收紧到族闸放行的行**(六审 finding① + 七审 finding① 采纳):
|
|
190
|
+
只拦身份腿是自相矛盾的(「同 taskId 任意行」兜底会把**同一行**重新捞出来);只剔除命中的那**一个
|
|
191
|
+
对象**也不够 —— 同 task 排着两条反族行时,兜底照样捡起第二条。冲突 = wire 数据自相矛盾
|
|
192
|
+
(park 说 gate 属 A 族、身份却指着 B 族的行),这一轮整体保守:只在族闸放行的行里选,一条不剩 ⇒
|
|
193
|
+
诚实 `no_pending`(走既有 fail-soft / 重呈)。🔴 收紧**只在冲突路径上**发生 —— 身份缺席时候选池
|
|
194
|
+
一字节不动(那条「任意行」兜底本来就是给 kind-only / 名字腿认不得的合法行留的)。
|
|
195
|
+
门:**F9-a/b/c/d/e** + **F10-h**(身份命中反族行 ⇒ 忽略身份)+ **F10-i**(kind-only 行的身份命中
|
|
196
|
+
照样算数 —— 族闸认一等 kind 腿)+ **F10-j/k**(队列只剩反族行 / 排着两条反族行 ⇒ 零 decide)。
|
|
197
|
+
- **🔴 [4933] 存量:决断成功臂 `dropHeld` 摘错扣留帧**(0.40.0「已知局限」里那条记案,本版销案):
|
|
198
|
+
决断成功那一下用的是 `findPendingForTask` 给的 callId —— 那是**卡面渲染的宽口**(同 taskId +
|
|
199
|
+
族谓词命中的第一行),不是身份;同族多行排队时它可能指向旁观者,于是被摘掉的是**另一条** call
|
|
200
|
+
的扣留帧,而那一帧从此不再吐(`flushHeld` 的「一帧不丢」在这一格上不成立 = dim 圆点永远等不到
|
|
201
|
+
结果行)。修(新动词 `dropHeldForDecidedPark`,两条腿):①**wire 身份**在场就点名摘 ——
|
|
202
|
+
server ≥7.41.0 的 `done{status:"suspended"}.result.toolCallId` 与 durable `suspended` 事件
|
|
203
|
+
`gate` 旁的同键([4913]/[4914] 请托① 到货件),两条 park 腿各读各的载体、判据同一只,
|
|
204
|
+
UNTRUSTED 值只认非空串(坏形按缺席回落);②缺席 ⇒ 只在候选于扣留表里**同族唯一**时才摘
|
|
205
|
+
(族的口径与取件谓词同轴:fs/shell 族看族成员集,其余看 toolName),同族多条 = 判不出主角 ⇒
|
|
206
|
+
**一条都不摘**,与 `flushHeld`「判不出整表不标」同轴的 fail-safe。
|
|
207
|
+
🔴 **身份必须从取件那一步就起作用**(异源复审 finding① 采纳):只拿它删帧、决断仍按队列首行走
|
|
208
|
+
= split-brain(决断绑定落在旁观者行、被删的是主角帧)—— 用户其实在批准/拒绝**另一个**工具调用,
|
|
209
|
+
而真 park 原地不动、反复重挂。所以 `park.gatedCallId` 一路传进两条取件腿
|
|
210
|
+
(`surfaceFsApprovalAndDecide` / `surfaceGateAndDecide` → `findPendingForTask` 的 `callId` 腿),
|
|
211
|
+
决断与删帧从此指向同一行;删帧那一步的 wire 身份因此降为**确证**(与真被决断的那一行相等才摘),
|
|
212
|
+
不再是第二个独立的摘取目标。**刻意不 fail-closed**:身份在场但队列里没有对应行 ⇒ 照旧回落既有
|
|
213
|
+
两条腿 —— 这个 join 本仓没有对真 server 的实证,而 fail-closed 的失效面是「每一次审批都决断不了」,
|
|
214
|
+
回落的失效面则与本键出现之前逐字节相同(代价不对称,方向倒向回落)。
|
|
215
|
+
🔴 **刻意不动**两处:`markDecided` 仍用 pending 行的 callId(它喂的是 #110 那条承重判据,候选
|
|
216
|
+
取自 `lastFsOrShellGatedCallId()`,两边必须同源);#324 的连坐判别子**不吃**这个新身份 ——
|
|
217
|
+
换锚是独立一件事,混在一起改就分不清谁引入了假阳。门:**F10-a..f**(含「决断绑定落在主角行、
|
|
218
|
+
旁观者一个字节没被碰」两条;既有 F8-S33 的期望串同批跟改:旧串只有一条正是因为旁观者的帧被
|
|
219
|
+
摘掉了,判词本身一字未变)。
|
|
220
|
+
|
|
221
|
+
**候上游到货的跟车项(本批不做)**:
|
|
222
|
+
|
|
223
|
+
- **core 5.55 `tool_end.gatedCallId`(顶层键;勘误:不在 details 面)**:到货后可给连坐簇的
|
|
224
|
+
**折叠归组**面升级(按 gate id 归组,而不是只按「本批」)。与本批的**识别**判据(errorCode)是
|
|
225
|
+
两件事 —— 识别不许锚 id(见 [4973]③),归组才用 id。届时另批跟车。
|
|
226
|
+
|
|
227
|
+
**已知局限(本版新增)**:
|
|
228
|
+
|
|
229
|
+
- **面板缓冲到顶时,最老的那条 lifecycle 会被挤掉**(七审 finding④ 处置后的**残余**):到顶必须有
|
|
230
|
+
东西让位,规则统一成**最新优先** —— 没有 progress 可让位时让最老那条(不论类别)。此前那版让
|
|
231
|
+
**进度**让位,而屏障之后的一条 progress 往往是某一行「新建/复活」的**唯一**一句话
|
|
232
|
+
(`projectFleetAgentRows` 只在变化时发帧),丢了它那一行在重挂后永远不存在 —— 那是更坏的丢法。
|
|
233
|
+
残余代价:一条 ~200 事件之前的 `end` 可能被挤掉(连同它的 `report`/`isError`);它要结的那一行由
|
|
234
|
+
每 turn 必发、且必然最新的 `sweep` 兜住。真正的根治是把缓冲从「原始事件日志」换成**按任务压实的
|
|
235
|
+
状态**,那会整体改写回放语义,自成一批,不搭 bug 修复批的车。
|
|
236
|
+
- **#324 判别子的 S3/S33 局限仍在**:同族多兄弟时「哪个撞了 gate」在**判别子**那一侧仍按诚实缺席
|
|
237
|
+
处置(整表不 stamp)。本版把 wire 身份接进来的只有 `dropHeld` 这一格 —— 判别子换锚要连着
|
|
238
|
+
`noteParkGate` 的证据结构一起动,单独排批。常驻门 `F8-S3/S33` 仍钉「全 ABSENT」。
|
|
239
|
+
- **`fleet-row` 缓冲合并按 (kind, taskId)**:同一个 taskId 的 `tick` 与 `fleet-row` **不**互相合并
|
|
240
|
+
(两条 lane 的可选性不同,消费端对它们的规矩也不同 —— 见 `EngineAgentPanelEvent` 的 fleet-row
|
|
241
|
+
臂头注)。面板未挂载期间同一行最多各留一条,这是刻意的。
|
|
242
|
+
|
|
243
|
+
## 0.40.0(2026-08-22)
|
|
244
|
+
|
|
245
|
+
**minor:连坐 abort 机读位 `_sema_collateral_abort`(#324 / [4907] 裁定)。**
|
|
246
|
+
|
|
247
|
+
- **🆕 连坐帧机读位 `_sema_collateral_abort`(#324)**:`gateLedger.flushHeld()` 对**已知不是 gate
|
|
248
|
+
主角**的 HOLD 帧 stamp additive 超集键 `_sema_collateral_abort: true`。`eventToSdkMessage` 的
|
|
249
|
+
`case 'tool_end'` 臂同批放行这一位到内部 `tool_end_result` 臂(严格 `true` 才铸,缺席不铸键)
|
|
250
|
+
——不放行就等于本包把**自己刚打上去**的事实在自己的包边界上剥掉,即该臂头注点名的
|
|
251
|
+
[1947] `toolCallId` / [2942] `governanceForced` / #263 两键同款病形的下一例。
|
|
252
|
+
病形:同 turn 多 call 在飞 + 一个撞 gate ⇒ 引擎对整批在飞 call 铸同一串 `Operation aborted`
|
|
253
|
+
⇒ HOLD 表按超集谓词全收 ⇒ 任一 flush 出口一次倾泻,端上满屏同文红行,而其中只有一条是用户
|
|
254
|
+
刚决断的那个 call。
|
|
255
|
+
🔴 **一帧不丢的不变量不变**:修的是「出口处分不清谁是谁」,不是丢帧 —— 丢帧的后果是永久
|
|
256
|
+
假排队(dim 圆点等不到结果行),比多渲一行更坏。
|
|
257
|
+
- **🔴 判别子在 `flushHeld` 当拍求值、且只认 park 供证(#324 换锚)**:出身**不**在入表当拍
|
|
258
|
+
(hold-poison 臂)冻结 —— durable park 腿的帧序是 `tool_start → tool_end(毒化) →
|
|
259
|
+
done{suspended}`,**gate 身份晚于毒化帧到达**,那一拍的任何判词都是猜的。按工具名猜双向都错:
|
|
260
|
+
一等 `kind==='tool_approval'` gate 的主角(名字腿认不得的 toolName)被判成连坐 = **假阳打在
|
|
261
|
+
主角上**;同族兄弟(两个 `Bash` 只有一个撞 gate)两个都被判成主角 = 连坐帧拿不到位。
|
|
262
|
+
🔴 判别子真正要回答的是「**这一批** abort 帧属于哪张 gate」,而 wire 上**没有任何位**做这个
|
|
263
|
+
关联。定谳:只有 **park** 具备结构性归属(`flushHeld` 的出口就长在那张 park 自己的决断/回退
|
|
264
|
+
路径上),所以判据**只从 park 取证** —— 同步 `tool_approval` 帧腿不供证、整条不标(已知局限二);
|
|
265
|
+
**pending 队列也不供证**(`durable checkpointGate` 不带 callId,`findPendingForTask` 取的是
|
|
266
|
+
「同 taskId + 谓词命中的第一行」,那是卡面渲染的宽口不是身份 —— 同族两行排队时它会给出旁观者,
|
|
267
|
+
拿它当主角就把真主角挤成「非主角」)。判据全部**批级**、一批一套:①本批恰一张 park(硬门;
|
|
268
|
+
durable 重放同一张不算新的一张,判据 = 自上次登记以来有没有**新 call** 进扣留表,且去重分支
|
|
269
|
+
一个字节都不写;ask park 臂同样要登记 —— 它是本批的第二张 park);②主角 = 本批(扣留帧 ∪ 已
|
|
270
|
+
出批帧)里**唯一**属该 park 族的 call,前置是每条扣留帧的 `tool_start` 本连接都见过。
|
|
271
|
+
🔴 出口方向 fail-safe:**判不出来 ⇒ 整表一个都不 stamp**(诚实缺席,字节零变),绝不按到达
|
|
272
|
+
顺序编判词。批级判据退休点两处:`flushHeld` drain 完、`dropHeld` 之后**无条件**检查扣留表为空
|
|
273
|
+
(写成「摘中了才查」则零毒化帧的 park 批永不退休,判据活到下一批派错判词)。
|
|
274
|
+
兜底腿读的是 tool_start 当拍写入的**单调集**,不是 `pendingFsCalls` —— 后者在 `routeToolEnd`
|
|
275
|
+
跑臂表之前就被 `dropFsCall` 摘掉了当前 callId,拿它做判据会退化成「除最后一张毒化帧外全判
|
|
276
|
+
连坐」的位置性裁决(主角被 stamp、真连坐豁免,方向恰好反)。
|
|
277
|
+
随之删除未发布窗内的 `HoldOrigin` 类型与 `hold()` 第三参、`heldOrigin()`(干净切,零已发布
|
|
278
|
+
消费者)。
|
|
279
|
+
异源对抗复审**四轮**逮到九条残留路径(八条是「假阳打回 gate 主角」,一条是判据自失效),
|
|
280
|
+
同批修 + 每条各配常驻反证:①兜底缺准入证(F8-S10);②身份 turn 级只增不删,多 hop 冒充已知
|
|
281
|
+
(F8-S11);③凭证在零 flush 路径滞留(F8-S12/S21);④ask park 不登记(F8-S13/S19/S25/S26);
|
|
282
|
+
⑤同批两个主角被「新 gate 清空身份」抹掉先到的那个(F8-S17);⑥durable 重放同一张 park 抹掉
|
|
283
|
+
刚登记的真身份(F8-S18);⑦历史主角在后一批变成连坐 call 时反向误标新主角(F8-S20);
|
|
284
|
+
⑧旧 server 同批双 gate 双主角(F8-S22);⑨同批混入两张真 park 时挑一个当主角(F8-S23);
|
|
285
|
+
⑩**零毒化帧的 park 批永不退休**(`dropHeld` 每次都落空 ⇒ 判据活到下一批,F8-S27);
|
|
286
|
+
⑪扣留帧的 `tool_start` 落在本连接之外时,兜底把**旁观者**当主角、反手给真主角打位(F8-S28);
|
|
287
|
+
⑫毒化帧与 park **一并重放**被当成第二张 park,本该带位的兄弟帧丢位(F8-S29,幂等性);
|
|
288
|
+
⑬去重的**反向**误判 —— 一张真的新 park 同样可以零毒化帧,它顺手刷新族属就把本批判据改成
|
|
289
|
+
另一族,兜底选中旁观者、反手误标真主角(F8-S30 ⇒ 去重分支一个字节都不写)。
|
|
290
|
+
⑭被去重掉的 park **借决断成功**把自己的 callId 登记为本批主角,把真主角挤成「非主角」
|
|
291
|
+
(F8-S31 ⇒ 采信答案经 `GatePark.parkAccepted` 一路带到决断腿)。
|
|
292
|
+
⑮`findPendingForTask` 的选行(谓词命中的**第一行**,不命中时退「任意行」)被当成身份 ⇒
|
|
293
|
+
别族 / 同族的旁观 call 被登记为主角、反手给真主角打位(F8-S32/S33 ⇒ **pending 队列一概不参与
|
|
294
|
+
判别**,决断结果只用于 gate 生命周期)。
|
|
295
|
+
十五条同源:**「本 turn 存在过一张 gate 的证据」不等于「这一批帧属于那张 gate」** —— 与 v1
|
|
296
|
+
名字锚同形(拿旁证冒充身份),故按同形存量宪法一并清,并补齐四条 park 臂的登记点。
|
|
297
|
+
门:`run-hitl-gate-honesty-test.mjs` F8 组 **S1–S33**(70→126 checks),每形态钉死判词全串
|
|
298
|
+
「谁带位谁不带」而不只是「有没有键」;**零已知假阳**是本批的验收硬线。
|
|
299
|
+
变异自证十六发全抓(逐发只由目标格抓红,cp 复原后逐字节 cmp 一致):废兜底唯一性⇒F7-①;
|
|
300
|
+
翻转 fail-safe⇒F8-S2;准入证恒放行⇒F8-S10;废「恰一张 park」硬门⇒F8-S23;废重放去重⇒F8-S18;
|
|
301
|
+
去重分支写族属⇒F8-S30;废 flush 侧退休⇒F8-S24;废摘空退休⇒F8-S16;退休改「摘中才查」⇒
|
|
302
|
+
F8-S27;废出批帧并集⇒F7-①;废 start 全覆盖前置⇒F8-S28;`hold` 覆盖也置位⇒F8-S29;
|
|
303
|
+
四条 park 臂逐臂不登记⇒F8-S25 / F7-① / F8-S26 / F8-S16。
|
|
304
|
+
|
|
305
|
+
**已知局限(本版新增)**:
|
|
306
|
+
|
|
307
|
+
- **端侧呈现半场不在本包**:本包只把机读位送到 `tool_end_result` 臂;「连坐簇怎么折叠」是各端
|
|
308
|
+
的呈现设计。壳(cli)的消费件在 1.0.90 窗跟车。
|
|
309
|
+
- **@core 对表位未闭**:core 5.51 `[DEFERRED]` / `gate.batch_halted`([4826]③)到货前,本键是
|
|
310
|
+
端能拿到的唯一机读判别子;到货后若 core 给出更强官方形,端侧换锚、本 additive 键保留兼容。
|
|
311
|
+
- **同族多兄弟 ⇒ 分不出主角**:两个 `Bash` 同时在飞、其中一个撞 gate 时,wire 上没有任何位能说
|
|
312
|
+
「哪个撞的」——`lastFsOrShellGatedCallId()` 给「最后一个」、pending 队列给「第一行」,都不是身份。
|
|
313
|
+
**决断成功也不例外**。此格按诚实缺席处置(整表不 stamp,端侧逐字节退回本键出现前)。
|
|
314
|
+
🔴 出路在 server:`durable checkpointGate` 带 `toolCallId`。常驻门 `F8-S3/S8/S33` 钉成「全
|
|
315
|
+
ABSENT」,server 补位后当场翻红,逼下一棒把新身份接进判据。
|
|
316
|
+
- **同步 `tool_approval` 帧腿整条不标(刻意为之)**:帧只证明「本 turn 出现过一张 gate」,证明
|
|
317
|
+
不了「这一批 abort 帧属于它」——那张 gate 可能早已正常收口(零毒化帧 ⇒ 零扣留表 ⇒ 一次 flush
|
|
318
|
+
都不跑),证却会活到下一批派判词。四轮复审里**全部**「假阳打在主角身上」的残留路径都出自帧腿
|
|
319
|
+
取证。取舍:**宁可少标,绝不误标主角**(误标主角=把用户真正该看的那条红行折叠掉,比多渲 N 行
|
|
320
|
+
更坏)。常驻门 `F8-S4/S5/S7/S11/S14/S17/S21/S22` 钉成「全 ABSENT」。🔴 出路同样在 server:
|
|
321
|
+
abort 批带 **gate 关联 id**,把归属从推断变成读数。
|
|
322
|
+
- **记案(存量,非本批根因):`dropHeld` 也在用不可信的 `gatedCallId`** —— 决断成功那一下按
|
|
323
|
+
pending 行的 callId 摘扣留帧,同族多行排队时可能摘掉**另一条** call 的帧,那一帧就再也不吐
|
|
324
|
+
(「一帧不丢」在这一格上不成立)。这条**早于本批**(与 4c42c8a 逐字相同),根因是「decide 结果
|
|
325
|
+
没有可核验的 park 身份」,与本批修的判别面是两件事;改它要动 #110 / REF-CC-034 的承重路径,
|
|
326
|
+
单独立案。server 补 `checkpointGate.toolCallId` 后两处一并收口。
|
|
327
|
+
- **缺席不是「这是主角」**:本键缺席同时覆盖「是主角」与「判别不出来」两种情况,端不得据此反推
|
|
328
|
+
主角身份(要断言主角,端自己按 `toolCallId` 与手上的审批卡对账)。已写进 §3z 缺席语义段。
|
|
329
|
+
|
|
31
330
|
## 0.39.0(2026-08-21)
|
|
32
331
|
|
|
33
332
|
**minor:tool_approval 帧窗三键过境 + 三新错误码消费件(0.38.0 已知局限「delegation.* 无机器可读载体」自本版解除)。**
|
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.41.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
|
|
@@ -245,6 +245,20 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
245
245
|
// (core 不鉴别、不比对、不回读)—— 只渲染,绝不回喂模型/工具入参,更不许当鉴权判据。
|
|
246
246
|
const toolEndSettledBy = ev.settledBy;
|
|
247
247
|
const toolEndApprover = ev.approver;
|
|
248
|
+
// ── #324 半场(0.40.0):连坐 abort 机读位 `_sema_collateral_abort`([4907] 裁定)──
|
|
249
|
+
// 出处**不是引擎**,是本包自己的 HITL 桥:`gateLedger.flushHeld` 对「出身 = collateral」的
|
|
250
|
+
// 扣留帧 stamp(见 `SEMA_COLLATERAL_ABORT_KEY` 头注)。桥在本臂**上游**(宿主先用
|
|
251
|
+
// `bridgeAskUserQuestionGates` 包住事件流,再喂 `eventToSdkMessage`),所以这一位到本臂时
|
|
252
|
+
// 已经在帧上了。
|
|
253
|
+
// 🔴 不转发 = 本臂白名单把它整段剥掉 ⇒ 宿主无论怎么写都读不到一条**本包自己刚打上去**的事实,
|
|
254
|
+
// 正是上面那段头注点名的 [1947] `toolCallId` / [2942] `governanceForced` / #263 两键
|
|
255
|
+
// 同款病形的下一例。缺席不铸键(严格 `true` 才铸,别的值一律当没有 —— 机读位是二值的,
|
|
256
|
+
// 「在场但不是 true」没有语义,折成在场会让消费端把一个坏形当成肯定)。
|
|
257
|
+
// 🔴 **只上内部臂,不进转录卡**:本臂是壳内部侧信道(见文件头注),转录 `tool_result` block
|
|
258
|
+
// 会随 `/compact` 直发 provider —— 往 block 上加自铸键正是 0.5.0 `label` 那个真缺口的形状
|
|
259
|
+
// (provider 400)。要让这一位进呈现面,归宿主在臂上读(壳 upstreamBridge 的 tee 就是
|
|
260
|
+
// `errorCode` 指路行同款既有位),不是往卡体里塞。
|
|
261
|
+
const collateralAbort = ev._sema_collateral_abort;
|
|
248
262
|
return projected(stamp(ctx, armBody({
|
|
249
263
|
type: 'tool_end_result',
|
|
250
264
|
toolCallId: ev.toolCallId,
|
|
@@ -267,6 +281,8 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
267
281
|
...(typeof toolEndApprover === 'string' && toolEndApprover.length > 0
|
|
268
282
|
? { approver: toolEndApprover }
|
|
269
283
|
: {}),
|
|
284
|
+
// #324:连坐 abort 机读位(严格 true 才上臂;见上方头注)。
|
|
285
|
+
...(collateralAbort === true ? { _sema_collateral_abort: true } : {}),
|
|
270
286
|
...(ev.eventId !== undefined ? { eventId: ev.eventId } : {}),
|
|
271
287
|
...(ev.parentToolCallId !== undefined
|
|
272
288
|
? { parentToolCallId: ev.parentToolCallId }
|
|
@@ -7,31 +7,6 @@
|
|
|
7
7
|
* 执行半场、判定走本包,是安全的拆缝(B6 T35/T36/T44 同款「判定半场搬入」姿势)。
|
|
8
8
|
* 逐字纪律:CLASSIFIER_DENY_SIGNATURE / NOTICE_REASON_MAX(80)/ 通知行三段格式一字节不改。
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
11
|
-
* src/sema/classifierVerdictWire.ts — auto-mode 分类器裁决帧的壳侧消费(分类器批3 壳半场,[907])。
|
|
12
|
-
*
|
|
13
|
-
* WIRE 形状(gap-check 2026-07-16 实勘,core 1.279 dist 亲核):
|
|
14
|
-
* 引擎侧分类器 block(core hooks.js runToolGate)→ decision = deny + decisionReason:"classifier",
|
|
15
|
-
* permissionDenied 钩子收 source:"classifier"(server 1.209 只作 metrics 计量,不出独立 deny 帧)——
|
|
16
|
-
* 壳能看见的唯一 wire 事实 = `tool_end` 帧:
|
|
17
|
-
* { type:'tool_end', isError:true, output: [{type:'text', text:'<system-reminder>\n
|
|
18
|
-
* auto-mode classifier blocked this call[: <reason>|: [<category>]]\n</system-reminder>'}] }
|
|
19
|
-
* (core hooks.js L138 deny message 逐字 + formatHookFeedback 包 system-reminder;
|
|
20
|
-
* agent-loop createErrorToolResult 落 content 数组;runtask toolOutputFrom 原样上 wire。)
|
|
21
|
-
* ⇒ 消费面按此签名结构性识别,等价于「deny 帧带 source:'classifier'」的判定(签名字符串是
|
|
22
|
-
* core 的机器产文面,非模型作文——模型 echo 不会以该签名做 tool_end 错误体开头)。
|
|
23
|
-
*
|
|
24
|
-
* 🔴 fail-soft 铁律:识别不中一律静默走原路,绝不伤害事件流。
|
|
25
|
-
* 🔴 UNTRUSTED:reason 为分类器(模型)产文,只渲染绝不回喂;通知行截断 ≤80(207 同款)。
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* 🟢 B8 ALLOW 清单登记(REF-CC-域词表-05,属主=client-core 规范重构轮,到期复议=core 改这句
|
|
29
|
-
* deny message 文案时或 2026-Q4 复审以先到者为准):文本匹配 + 非文本结构性合取前置
|
|
30
|
-
* (`ev.type==='tool_end' && ev.isError===true`,见 `classifierDenyFromToolEnd`)。同批同表的
|
|
31
|
-
* 另两处:`detachWire.ts` `DETACH_DURABLE_OFF_400_ANCHOR`(server 契约话)、
|
|
32
|
-
* `subagent/engineTaskHandleWire.ts` `STOP_CONFLICT_CODES`(SDK typed 缺席回落)——三处互指,
|
|
33
|
-
* 理由逐条写在各自站点。
|
|
34
|
-
*/
|
|
35
10
|
/** core hooks.js 分类器 deny message 的机器签名(逐字锚;lockstep:core 改文案这里必须跟)。 */
|
|
36
11
|
export declare const CLASSIFIER_DENY_SIGNATURE = "auto-mode classifier blocked this call";
|
|
37
12
|
export interface ClassifierDenyVerdict {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* `subagent/engineTaskHandleWire.ts` `STOP_CONFLICT_CODES`(SDK typed 缺席回落)——三处互指,
|
|
33
33
|
* 理由逐条写在各自站点。
|
|
34
34
|
*/
|
|
35
|
+
import { unwrapSystemReminder } from './systemReminderTag.js';
|
|
35
36
|
/** core hooks.js 分类器 deny message 的机器签名(逐字锚;lockstep:core 改文案这里必须跟)。 */
|
|
36
37
|
export const CLASSIFIER_DENY_SIGNATURE = 'auto-mode classifier blocked this call';
|
|
37
38
|
/** 通知 reason 段截断(CC 207 L696763 同款 80 → 79+…)。 */
|
|
@@ -52,11 +53,22 @@ function textFromWireOutput(output) {
|
|
|
52
53
|
.filter((t) => t !== undefined);
|
|
53
54
|
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
/**
|
|
57
|
+
* core 分类器 deny 的**三个**铸点(core 5.52.0 dist 逐字枚举,候选③ [4974]):
|
|
58
|
+
* ① `core/hooks.js:606` —— 基础形;
|
|
59
|
+
* ② `core/runner/prepare-task.js:3103/3192` —— 继承自祖先层形;
|
|
60
|
+
* ③ `core/runner/prepare-task.js:3023` —— 「批准过的编辑」在祖先层被拦形。
|
|
61
|
+
* 三者尾巴同形:`: ${reason}` 或 `: [${category}]`(都没有时整条到此为止)。
|
|
62
|
+
*
|
|
63
|
+
* 🔴 **顺序即语义**:① 是 ② 的**前缀**,短的排在后面 —— 反过来的话 ② 会被 ① 蒙对,而 `tail` 变成
|
|
64
|
+
* ` at an inherited ancestor layer: …`,不以 `': '` 开头 ⇒ **reason 整条丢失**(修前实况)。
|
|
65
|
+
* 🔴 逐字锚,不做模糊匹配(B8 ALLOW 登记的既有纪律):core 改文案这里必须跟车。
|
|
66
|
+
*/
|
|
67
|
+
const CLASSIFIER_DENY_FORMS = [
|
|
68
|
+
'auto-mode classifier blocked the approved edit at an inherited ancestor layer',
|
|
69
|
+
'auto-mode classifier blocked this call at an inherited ancestor layer',
|
|
70
|
+
CLASSIFIER_DENY_SIGNATURE,
|
|
71
|
+
];
|
|
60
72
|
/**
|
|
61
73
|
* 结构性识别一张 tool_end 帧是否为分类器 deny 裁决帧。
|
|
62
74
|
* 判定 = isError:true ∧ 错误体(去包裹后)以 core 机器签名开头。识别不中 ⇒ undefined(原路)。
|
|
@@ -67,10 +79,18 @@ export function classifierDenyFromToolEnd(ev) {
|
|
|
67
79
|
const raw = textFromWireOutput(ev.output);
|
|
68
80
|
if (raw === undefined)
|
|
69
81
|
return undefined;
|
|
82
|
+
// 件⑥([4974]):剥壳走**单一闸口** —— core ≥5.46 的开标签恒带 `mark="…"`,而这里原来锚的是
|
|
83
|
+
// 裸开标签 ⇒ 恒剥不掉 ⇒ 下面的 startsWith 恒不成立 ⇒ 整条 auto-deny 呈现面静默失效。
|
|
84
|
+
// 口径与另一座位(`subagent/engineDelegatedPrompt`)共用一份,见该模块头注。
|
|
70
85
|
const message = unwrapSystemReminder(raw).trim();
|
|
71
|
-
|
|
86
|
+
// 🔴 前缀**不是**语法(七审 finding③):命中之后还要看**边界**。core 的铸点尾巴只有两种形 ——
|
|
87
|
+
// 到此为止,或紧跟 `': '`(reason / `[category]` 都走这一个分隔)。少了这条,
|
|
88
|
+
// `…ancestor layerX` 这类近似串也会被判成分类器裁决,而那条路径会 `markEnded` + 摘 HOLD +
|
|
89
|
+
// 写 Recent Denials + 用裁决原文覆盖渲染 —— 任何能产出工具错误正文的工具都能伪造。
|
|
90
|
+
const form = CLASSIFIER_DENY_FORMS.find((f) => message.startsWith(f) && (message.length === f.length || message.startsWith(': ', f.length)));
|
|
91
|
+
if (form === undefined)
|
|
72
92
|
return undefined;
|
|
73
|
-
const tail = message.slice(
|
|
93
|
+
const tail = message.slice(form.length);
|
|
74
94
|
const reason = tail.startsWith(': ') ? tail.slice(2).trim() : undefined;
|
|
75
95
|
return { message, ...(reason !== undefined && reason.length > 0 ? { reason } : {}) };
|
|
76
96
|
}
|
|
@@ -62,26 +62,115 @@ export function publishEngineAgentPanelEvent(ev) {
|
|
|
62
62
|
}
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
// Buffered (panel not mounted yet / temporarily unmounted).
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// Buffered (panel not mounted yet / temporarily unmounted).
|
|
66
|
+
//
|
|
67
|
+
// Two event CLASSES, and every buffering rule below keys off the class (not off one kind):
|
|
68
|
+
// · PROGRESS ('tick' / 'fleet-row') — cumulative rollups off the two live lanes. Only the latest
|
|
69
|
+
// one per taskId carries information, so they COALESCE per (kind, taskId) and they are the ones
|
|
70
|
+
// that yield when the buffer is full.
|
|
71
|
+
// · LIFECYCLE ('end' / 'sweep') — each one is a distinct fact that settles a row. They are never
|
|
72
|
+
// the ones squeezed out by a long unmount (adversarial review w0zwpa251 #4: dropping a buffered
|
|
73
|
+
// 'end' left a row running forever).
|
|
74
|
+
//
|
|
75
|
+
// 🔴 [F4957-2] (0.41.0) the full-buffer arm used to look ONLY for a 'tick' to evict and then pushed
|
|
76
|
+
// UNCONDITIONALLY even when it found none — so a stream carrying no ticks at all (the 'fleet-row'
|
|
77
|
+
// lane is exactly that: background subagents never reach the panel through 'task_progress', see the
|
|
78
|
+
// fleet-row arm's own header) grew the buffer without bound. MAX_BUFFER is a memory bound on
|
|
79
|
+
// UNTRUSTED wire volume; it has to hold for every kind mix, not just the one the first version had
|
|
80
|
+
// in mind.
|
|
81
|
+
// Adjacent 'sweep' dedup. A sweep settles every row the consumer still has running and its own doc
|
|
82
|
+
// calls it idempotent, so a sweep whose immediate predecessor is a sweep is a strict no-op — nothing
|
|
83
|
+
// happened in between that it could settle differently. Dropping it is not a micro-optimisation: a
|
|
84
|
+
// long unmount publishes one sweep per turn, and 200 of them fill the buffer with pure lifecycle
|
|
85
|
+
// events, after which every post-barrier progress event has to yield (that is the other half of the
|
|
86
|
+
// bounded policy) — a subagent that starts at that point would replay as "still settled", forever.
|
|
87
|
+
if (ev.kind === 'sweep' && buffer.length > 0 && buffer[buffer.length - 1]?.kind === 'sweep')
|
|
88
|
+
return;
|
|
89
|
+
const progress = ev.kind === 'tick' || ev.kind === 'fleet-row';
|
|
90
|
+
if (progress) {
|
|
91
|
+
const kind = ev.kind;
|
|
92
|
+
const taskId = ev.taskId;
|
|
93
|
+
// Search BACKWARDS for a merge target and STOP at the first barrier. The bar for the whole buffer
|
|
94
|
+
// is "replaying it leaves the consumer in the same state as if the panel had been mounted all
|
|
95
|
+
// along", and each barrier below is a place where the consumer's state machine changes what a
|
|
96
|
+
// later event means — so an event on the far side of one cannot be folded into an event on this
|
|
97
|
+
// side (0.41.0 adversarial review #5 and #6):
|
|
98
|
+
// · 'sweep' (global) and 'end' for this taskId — they SETTLE the row, and the consumer ignores
|
|
99
|
+
// progress for a settled row. Live: `tick(v1) → sweep → tick(v2)` builds the row, settles it,
|
|
100
|
+
// then drops v2 → the row ends settled. Folded into `sweep → tick(v2)` the replay sweeps an
|
|
101
|
+
// empty table and then v2 BUILDS a running row — a ghost that live delivery never produces.
|
|
102
|
+
// · the OTHER progress lane for this taskId — the fleet-row arm's own header states that a
|
|
103
|
+
// 'tick' makes the fleet lane hand the row over PERMANENTLY, so what the fleet lane said
|
|
104
|
+
// BEFORE the handover (transcriptId / startedAt, often carried by exactly one frame) would
|
|
105
|
+
// land after it and never be applied.
|
|
106
|
+
// Barriers cost buffer slots, and that is the right trade: the bound is still enforced by the
|
|
107
|
+
// eviction policy below, while a wrong fold is silently wrong forever.
|
|
108
|
+
let i = -1;
|
|
109
|
+
for (let at = buffer.length - 1; at >= 0; at--) {
|
|
110
|
+
const b = buffer[at];
|
|
111
|
+
if (b === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
if (b.kind === 'sweep')
|
|
114
|
+
break;
|
|
115
|
+
if (b.kind === 'end') {
|
|
116
|
+
if (b.taskId === taskId)
|
|
117
|
+
break;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (b.taskId !== taskId)
|
|
121
|
+
continue; // another task's progress — irrelevant to this row
|
|
122
|
+
if (b.kind === kind)
|
|
123
|
+
i = at;
|
|
124
|
+
break; // same lane ⇒ merge target; other lane ⇒ barrier
|
|
125
|
+
}
|
|
71
126
|
if (i >= 0) {
|
|
72
|
-
|
|
127
|
+
// 🔴 Coalescing must not DROP sticky keys either (0.41.0 adversarial review #4). Both progress
|
|
128
|
+
// lanes are published as PARTIAL frames — the projection emits `...(toolUses !== undefined ?
|
|
129
|
+
// { toolUses } : {})` and friends, and an absent key means "this frame didn't say, keep what the
|
|
130
|
+
// row already has" (the three-state rule on the fleet-row arm above). A mounted panel therefore
|
|
131
|
+
// still shows the `toolUses` that arrived on v1 after a v2 that only moved `totalTokens`; a
|
|
132
|
+
// replay that kept only v2 would lose it for good. So the surviving copy is a per-key merge
|
|
133
|
+
// (later frame wins per key, keys it omits are inherited), not a wholesale replacement.
|
|
134
|
+
const merged = { ...buffer[i], ...ev };
|
|
135
|
+
// 🔴 Coalescing must not REORDER across a lifecycle barrier (0.41.0 adversarial review #2).
|
|
136
|
+
// Overwriting in place rewrites `progress(A,v1) → end(A) → progress(A,v2)` into
|
|
137
|
+
// `progress(A,v2) → end(A)` — the post-settle rollup is moved BEFORE the settle, so a row that
|
|
138
|
+
// was revived (the fleet projection explicitly allows a settled row to run again) gets settled
|
|
139
|
+
// by the replay again, with the revived progress applied to the dead cycle. The bar for a
|
|
140
|
+
// buffered replay is "same result as if the panel had been mounted the whole time", so the
|
|
141
|
+
// surviving copy has to keep the LATEST event's arrival position: drop the old one, append.
|
|
142
|
+
buffer.splice(i, 1);
|
|
143
|
+
buffer.push(merged);
|
|
73
144
|
return;
|
|
74
145
|
}
|
|
75
146
|
}
|
|
76
|
-
if (buffer.length < MAX_BUFFER)
|
|
147
|
+
if (buffer.length < MAX_BUFFER) {
|
|
77
148
|
buffer.push(ev);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Full: the oldest PROGRESS event yields (its taskId's next rollup supersedes it anyway).
|
|
152
|
+
const evictAt = buffer.findIndex(b => b.kind === 'tick' || b.kind === 'fleet-row');
|
|
153
|
+
if (evictAt >= 0) {
|
|
154
|
+
buffer.splice(evictAt, 1);
|
|
83
155
|
buffer.push(ev);
|
|
156
|
+
return;
|
|
84
157
|
}
|
|
158
|
+
// Nothing but lifecycle events in a full buffer (MAX_BUFFER rows settled while the panel stayed
|
|
159
|
+
// unmounted — 200 distinct 'end's is the realistic way there, the adjacent-sweep dedup above having
|
|
160
|
+
// removed the cheap one). At a finite bound something must give, and the rule is NEWEST-WINS for
|
|
161
|
+
// every kind: the oldest event yields, whatever it is.
|
|
162
|
+
//
|
|
163
|
+
// 🔴 Why not "progress yields here too" (which is what this arm used to do — adversarial review
|
|
164
|
+
// round 7, finding #4): a progress event arriving at this point is the ONLY word the panel will
|
|
165
|
+
// ever get about a row that was created or revived after the barrier — `projectFleetAgentRows`
|
|
166
|
+
// emits on CHANGE, so a row that then goes quiet never speaks again. Dropping it makes that row
|
|
167
|
+
// permanently invisible after a remount, which is a strictly worse loss than dropping the OLDEST
|
|
168
|
+
// lifecycle event: that one settles a row that finished ~MAX_BUFFER events ago, and the sweep that
|
|
169
|
+
// every turn publishes (newest ⇒ always survives) settles whatever is still running anyway.
|
|
170
|
+
// The residual — an old 'end' can be dropped, taking its `report`/`isError` with it — is the honest
|
|
171
|
+
// price of a bounded buffer and is registered in CHANGELOG 0.41.0 "已知局限".
|
|
172
|
+
buffer.shift();
|
|
173
|
+
buffer.push(ev);
|
|
85
174
|
}
|
|
86
175
|
/** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
|
|
87
176
|
export function subscribeEngineAgentPanel(fn) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ## 先纠一个事实(census G20 的措辞会误导下一棒)
|
|
7
7
|
*
|
|
8
8
|
* G20 原话是「`hitlBridge.ts:197` 等三处今天靠 `approvals.list` 轮询」。**实测不是轮询** ——
|
|
9
|
-
* 那三处(`hitlBridge.
|
|
9
|
+
* 那三处(`hitlBridge.pendingForDecide` / `askGateWire.surfaceGateAndDecide` /
|
|
10
10
|
* `toolApprovalWire.surfaceFsApprovalAndDecide`)都是**按需单次取件**:流上已经来了 park 信号,
|
|
11
11
|
* 才去取那一行。真正缺的不是「把轮询换成推送」,而是**根本没有推送通道**:
|
|
12
12
|
* · 别的客户端(web/桌面/另一个 TUI)解掉了一条 pending —— 本端不知道;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ## 🔴 它**不替换** D-1 的取件(这条是安全边界,别"顺手优化"掉)
|
|
19
19
|
*
|
|
20
|
-
* D-1
|
|
20
|
+
* D-1 两元组必须取自**人看见的那一行**。让取件去读一个可能陈旧的快照 =
|
|
21
21
|
* 用一个过期的 `boundInputHash` 去 decide ⇒ server fail-closed 409 ⇒ 一个合法审批被拒
|
|
22
22
|
* (或者更坏:快照比服务端新旧关系不确定时,人看到的和绑定的不是同一件事)。
|
|
23
23
|
* 所以那三处**继续走权威 `list()`**,本 feed 只做「有变化了」的通知面与快照面。
|