@sema-agent/client-core 0.37.0 → 0.38.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +159 -14
  2. package/README.md +3 -2
  3. package/dist/adapt/arms.js +40 -0
  4. package/dist/adapt.d.ts +1 -1
  5. package/dist/adapt.js +2 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +1 -1
  7. package/dist/adapter/activeRunSelfHeal.js +6 -6
  8. package/dist/adapter/downstream/eventToSdkMessage.js +94 -0
  9. package/dist/agentSession/backgroundView.d.ts +1 -1
  10. package/dist/agentSession/backgroundView.js +1 -1
  11. package/dist/engineCapsCache.d.ts +33 -1
  12. package/dist/engineCapsCache.js +26 -4
  13. package/dist/engineErrorCodes.d.ts +20 -0
  14. package/dist/engineErrorCodes.js +44 -0
  15. package/dist/fleet/fleetLedger.d.ts +1 -1
  16. package/dist/fleet/fleetLedger.js +3 -3
  17. package/dist/fleet/fleetProjection.d.ts +34 -0
  18. package/dist/fleet/fleetProjection.js +40 -0
  19. package/dist/hitl/armedGateRegistry.d.ts +1 -1
  20. package/dist/hitl/armedGateRegistry.js +2 -2
  21. package/dist/hitl/hitlBridge.js +77 -18
  22. package/dist/hitl/persistedRulesWire.d.ts +1 -1
  23. package/dist/hitl/persistedRulesWire.js +4 -4
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/model/modelSupplyRules.d.ts +102 -0
  27. package/dist/model/modelSupplyRules.js +149 -0
  28. package/dist/model/providerPresets.js +68 -12
  29. package/dist/request/taskRequest.d.ts +1 -1
  30. package/dist/request/taskRequest.js +8 -8
  31. package/dist/seam.d.ts +48 -1
  32. package/dist/seam.js +7 -0
  33. package/dist/subagent/engineSubagentResume.d.ts +20 -2
  34. package/dist/subagent/engineSubagentResume.js +8 -1
  35. package/dist/subagent/engineSubagentSteer.d.ts +1 -1
  36. package/dist/subagent/engineSubagentSteer.js +1 -1
  37. package/dist/subagent/subagentOwnerAbsence.js +1 -1
  38. package/dist/subagentContentStore.js +1 -1
  39. package/dist/wireErrorTriage.js +1 -1
  40. package/docs/INTEGRATION-CLIENTS.md +70 -20
  41. package/docs/REFACTOR-LEDGER.md +2 -2
  42. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -23,11 +23,156 @@
23
23
  > 既有行)⇒ 标题**不回改、且永远不会被改**(`b165f3f` 那个 commit 的字节是历史,任何未来提交
24
24
  > 都改不到它)⇒ 这条勘误与门侧窄豁免(`run-integration-doc-freshness-test.mjs` ④b
25
25
  > `KNOWN_HEADING_ERRATA`,登记 `version: '0.36.0', releasedAt: 'b165f3f'`)**都是永久的**,不是
26
- > 「下一版删掉」的临时态(codex 复审 finding①:那样写会让豁免一删,门在**任何**后续版本上
26
+ > 「下一版删掉」的临时态(对抗复审 finding①:那样写会让豁免一删,门在**任何**后续版本上
27
27
  > 都会重新对这个永久冻结的标题判红,退休条件不可能被满足)。门侧核验两件事把这条勘误钉死、
28
28
  > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
29
  > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
30
30
 
31
+ ## 0.38.1(2026-08-21)
32
+
33
+ **patch:纯出包卫生版,零 API/行为变化。**
34
+
35
+ - **出包面词汇消毒**:dist 注释(tsc 编译保留注释)与随包 Markdown 全量按发布词汇规范清理;
36
+ 历史 CHANGELOG 段同批词面映射(零语义改动),冻结账按消毒纪元(commit `a0488b2`)重钉,
37
+ `run-integration-doc-freshness` ④a2s 新增消毒等价核 —— 借消毒偷加行/改语义照样红。
38
+ - **新门 `run-package-hygiene`**:`package.json` `files` 全实体(dist js/d.ts + 随包 md)逐行过
39
+ 发布词汇禁表(自检 + allow 双向),gates-manifest 32 套。
40
+
41
+ ## 0.38.0(2026-08-21)
42
+
43
+ **#318 四件合批 + SDK 7.2.0 提货跟车。行为面五条**:一条新投影臂(additive)、一条超时预算收窄
44
+ (口径换代)、一条分层遮蔽真修(修前那条分支恒不可达)、两条**可分辨读口/分类**的加细。**
45
+
46
+ > 📌 **公面基线 753 → 764**(npm 上的 `0.37.0` 是 753)。11 个新导出全部 **additive**,零 BREAKING:
47
+ > `engineCapState`(+ 类型 `EngineCapState`)· `resolveEntryVision` / `computeDeleteBlockers` /
48
+ > `computeDeleteWarnings`(+ 四个具名类型)· `CONFIG_DELEGATION_ENTRY_CAPS` /
49
+ > `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP` / `DELEGATION_CAP_CODES` /
50
+ > `isDelegationCapCode` · `wireCycleSeq` / `wireRetiredBy`。
51
+ > 🔴 **peer 地板抬到 `>=7.2.0`**(见下「SDK 7.2.0 提货」);装 <7.2.0 SDK 的端提货前先抬依赖。
52
+
53
+ - **🆕 件① `engine_notice` 投影臂(additive,server ≥7.36 / #310;协作板 [4679] 发布、[4682] cli 认领)**:
54
+ 引擎的结构化通告里被 server 判为面向**本会话终端用户**的那一小撮,现在有了投影口 ——
55
+ `eventToSdkMessage` → 中性内部臂 `engine_notice` → `adapt()` → chrome 臂
56
+ `{ kind:'engine_notice', code, message, detail, sessionId?, ts?, eventId? }`。**不铸 transcript 行**
57
+ (通告是披露不是转录物,硬铸一行就是替引擎编一句它没说过的话)。
58
+ 🔴 **开集是本件的承重设计,不是风格**:本层**一个码都不硬编** —— 没有识别表、没有 switch、
59
+ 没有「已知才投」。server 的白名单会随 core 码册增长(起步三码 → core 5.47/5.48 的
60
+ `NOTICE_AUDIENCE` 到货后 hold 三码入册),任何在客户端复制一份白名单的实现都会在下一次
61
+ core 加码的当天开始静默丢帧。同理**不读能力位**:帧到了就投影(A-022「到帧即服务」),
62
+ 拿一个探测位去 gate 一条**已经到手的事实**,只会在探测未判/失败时把真事实丢掉。
63
+ `detail` **整对象逐字透传**(禁挑键):逐码不同且开集,挑键 = 新码的事实在本层静默蒸发。
64
+ 消费纪律五条(按 code+detail 渲 / message 仅 fallback / 未知码不许丢 / durable 重放幂等 /
65
+ `harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**)写在 `seam.ts` 臂注释与
66
+ `docs/INTEGRATION-CLIENTS.md` §3f。**壳渲染半场不在本批**(cli 0.38.0 提货时做)。
67
+ 🔴 **重放身份两个键(对抗复审 [medium] 采纳,当窗修)**:首版只带 `eventId`,于是 body 不带
68
+ 它时留给端的唯一去重口是 `code+ts` —— 而 `ts` 是 **server 观察时刻(ms)**,同毫秒同码的两条真通告
69
+ 会被折成一条、跨重连的同一条又可能重复。而 SDK 的 SSE parser **本来就把 durable `id:`
70
+ (= `task_event.seq`)stamp 到 `ev.id` 上**(`dist/sse.js`),本包也早有规范访问口
71
+ (`adapter/types.eventSeq`)—— 有稳定序号却不带 = 本层自己把可靠性降级了。现补 `eventSeq` 位,
72
+ 幂等序 = `eventId` > `eventSeq` > `code+ts`。🔴 **两键绝不合并**
73
+ ([same-name-different-meaning-crosses-layers]:全局身份 vs per-task 序号)。
74
+ ⚠️ **如实登记两条**(2026-08-21 亲验,接入文档宪法):① `engine_notice` **尚未进已发布 SDK 的
75
+ `AgentEvent` union** —— sdk 仓 `3d6aebc` 已写,但 npm `@sema-agent/sdk@7.2.0` 真 tarball 的 `dist/`
76
+ 全树零命中(同批的 `FleetTaskRow.cycleSeq` 在场,证明抽检会说话)。故按 `workflow_complete` /
77
+ `human_input` 当年的先例走 **raw 预分派**,并留自退休钉:臂一进 union,`assertNeverArm` 编译期
78
+ 真红,逼下一棒搬进 switch。② server 的 `ASSISTANT-WIRE-CONTRACT` 附录 D.3 仍写「起步白名单三码」,
79
+ 而 server main 的 `ENGINE_NOTICE_WIRE_CODES` 已是六码 —— **对本包与端零影响**(正因为消费面按
80
+ 开集写),已按宪法回报 server。
81
+ - **🔴 件② `decide` 超时重试总窗收窄:10 分钟 → 90 秒(口径换代,不是调参)**。
82
+ 旧值按 [4664] 的**长调用**口径开:legacy 任务级 decide 在响应前同步跑整条 resume(模型往返无上限),
83
+ 60s 帽结构性必炸,所以超时是**常态**。server #316([4687],随 7.37.0 发车)把 200 体从终局形改成
84
+ **受理回执**(受理点在 core `resumeStream` 解析后,一切会变成拒绝的判定仍同步发生;实测受理即回
85
+ ≈23ms)⇒ **超时从常态变真异常**,总窗随之收窄。
86
+ 🔴 **新值锚在「真正决定发数的量」上而不是拍一个整数**:SDK 的 per-attempt 帽(60s)本包不可配,
87
+ 而每一发超时类失败本身就吃满一个帽 ⇒ 实际发数由「总窗 ÷ 帽」决定且量化很粗 —— 窗 ≤1 帽 = **零重试**
88
+ (重试环变死结构)、>2 帽 = 3 发起步一路回到分钟级、(1 帽, 2 帽] = **恰一次重试**。取
89
+ `60_000 × 1.5`(**写成「帽 × 系数」而不是裸 90000**,SDK 改帽时推导自动跟手),落在区间中部而不
90
+ 贴边界。可预算最坏墙钟 ≈121s。**重试环结构一字未动**(超时臂仍走窗判),[4675] 的「绝不造第二层
91
+ 节奏叠乘」照旧成立 —— 本批没有新增任何计时器/退避层。
92
+ 🔴 **两条残余同步形如实登记**(收窄不等于长调用消失了,[4687] 逐字):终局形仍存在于 ①pre-7.37
93
+ 的 server、②≥7.37 但没有 durable run 行可跟的部署。本包是三端共用件,故重试环保留、总窗不设 0;
94
+ 这两形上窗尽的代价是**重呈臂**(同一张卡重交用户)而不是把 turn 判死。
95
+ 🔴 **「至少重试一次」改由发数保证(对抗复审 [medium] 采纳,当窗修)**:per-attempt 帽是
96
+ **宿主构造 client 时**定的(本桥吃注入 client,`HitlClientLike` 根本不暴露 `timeoutMs`)⇒
97
+ 「总窗 ÷ 帽 = 发数」这条推导只在 SDK **缺省值**上成立,不能当本共用件的性质 —— 一个 120s client
98
+ 的宿主在首发超时那一刻 elapsed 已 ≥ 窗,**一次重试都拿不到**,而上面那两条残余同步形恰恰最需要
99
+ 它。故补第二个界 `DECIDE_TIMEOUT_MIN_ATTEMPTS = 2`,终止条件改为**合取**(发数达标 ∧ 窗尽)。
100
+ ⚠️ 这**不是** [4675] 的「第二层节奏叠乘」:零新增定时器/退避层,退避仍是同一条 backoff 链,
101
+ 只是同一个循环上的第二个终止条件。三种 client 配置都有界且都拿得到那次重试:120s ⇒ 2 发/~240s、
102
+ 缺省 60s ⇒ 2 发/~121s、30s ⇒ 3 发/~92s。常驻套件同批加 F6-b4(窗=0 且每发都超时 ⇒ 必须恰在
103
+ 第 2 发后耗尽),F6-b3 的期望值随行为改动同批更新并写明数字变化的来由。
104
+ - **🔴 件③(a)`inferFamily` 的 `vision` 分层遮蔽修 —— 一条恒不可达的分支**(cli [4752] 自领缺口1)。
105
+ 病:`vision` 位只写在 `modelFamilies.json` 上,而那张表只有**第④层**读;可任何真实的 `deepseek-*`
106
+ id 在**第①层**(preset 大表精确)或**第③层**(家族主干包含)就已命中并 return,而这两层的构造器
107
+ `hitOf` 不带 vision 位 ⇒ 第④层结构性不可达 ⇒ `deepseek4` 行的 `vision: false` **永远读不出来**,
108
+ 下游 `MODEL_VISION=false` stamp 恒不发生(两处注释自述的行为是死的)。
109
+ 修的形 = 把 `vision` 从「第④层的一个字段」提成**与容量证据层正交的一次独立查表**:容量轴
110
+ (ctx/maxTokens/perModelCap)四层证据强者先赢,**一字不动**;vision 轴恒查 family 表,与哪一层命中
111
+ **无关**。理由:preset 大表根本没有 vision 列,主干匹配拿的是**别的模型**的行 —— 两者都不是
112
+ 「这个 id 能不能看图」的证据。🔴 **诚实缺席不变**:表上没标 ⇒ 键不 stamp(`kimi`/`qwen` 仍缺席),
113
+ 绝不顺手补成 `false` 去封死一个真能力。
114
+ ⚠️ **新契约形(cli 侧套内已钉前提断言,提货时同批改)**:`inferFamily(id)?.vision` 现在对**任何**
115
+ 证据层的命中都可能在场(此前只有第④层),`vision` 与 `perModelCap` 的在场性**互相独立**。
116
+ - **🆕 件③(b)`engineCapState(baseUrl, key)` —— 能力位的可分辨读口(additive)**(cli [4752] 自领缺口2)。
117
+ `engineCapTrue` 只回 true/false,`false` 同时承载「引擎明说没有」「还没探」「探测在飞」「探测失败」
118
+ 四件事。对**放行判据**这个塌缩刻意且正确(fail-closed);对**自检/诊断面**(doctor)它是谎报 ——
119
+ 这也是 doctor 此前无法复用共享缓存的直接原因。新读口给闭集五态:`true`/`false`(引擎明说)、
120
+ `unprobed`(没有已落地的 caps;四种成因**故意合并**,因为对调用方是同一个动作)、`absent`
121
+ (caps 已落地但没这个键)、`non_boolean`(键在但值不是布尔 —— 不并进 `absent`,那会是假话)。
122
+ 🔴 **放行语义一字未变**:`engineCapTrue` 现在就是 `engineCapState(...) === 'true'` 的单源实现。
123
+ - **🆕 件③(c)Model Hub 供给面三纯函数上收(additive)**:`resolveEntryVision` /
124
+ `computeDeleteBlockers` / `computeDeleteWarnings` 从 cli `modelChannels.ts` 搬进
125
+ `model/modelSupplyRules.ts`(新模块,**零 IO**)。三端 Model Hub 要回答的是同样三个问题
126
+ (vision 生效值+来源 / 删了会不会断链 / 删了会不会悄悄改语义),各写一遍 = 三个 Hub 对
127
+ 「同一条档能不能删」给出三个答案,而其中两个要等用户按下 y 之后才被发现。
128
+ 读盘那半场留各端;`doc === null` 的两义(没读到 vs 读不出来)**在调用方分流** —— 读不出来时
129
+ 调用方自产 `unreadable` 阻断,fail-closed 拒删比「猜没人指着」安全。
130
+ ⚠️ **上收差分两条(行为零改动)**:`resolveEntryVision` 的返回型由内联匿名对象改为具名
131
+ `EntryVisionResolution`(本仓 typeshape 门 B4 棘轮),两个 compute 的入参同理具名 —— **结构逐字
132
+ 相同**,cli 剪切 import 时零适配。🔴 与件③(a)**同批落地是刻意的**:修前
133
+ `inferFamily(deepseek-*)?.vision` 结构性恒 `undefined`,`source:'family'` 这一档对 deepseek 族
134
+ **从来没走到过** —— 只上收不修 = 把一条死分支原样搬进三端。
135
+ - **🆕 件④ 两族 coded 拒绝(additive;[4743] core 5.48.0 @cli 点名件的 client-core 半场)**:
136
+ ① **委派席位到限两码** `delegation.concurrency_cap` / `delegation.session_cap`(core 5.48.0
137
+ design/323 的 `RunnerDeps.delegationEntryCaps`,CC 对齐缺省 20/200)入词表,配 `DELEGATION_CAP_CODES`
138
+ 识别表 + `isDelegationCapCode` 开集前缀谓词。🔴 **两码处置不对称,禁合并**:并发帽 = **可等**
139
+ (兄弟结束就有位,同一条命令过一会儿照样成)/ 会话累计帽 = **等也没用**(这棵树这辈子的配额用完了)
140
+ —— 合并成一句「委派失败」会让一半用户去干等、另一半去改配置。
141
+ ② `config.delegation_entry_caps`(坏旋钮响亮拒)入 `CONFIG_REFUSAL_CODES` 识别表(前缀谓词本来
142
+ 就认它;识别表是**登记**,让提货 diff 里一眼看出这版加了哪个词)。
143
+ ③ **SendMessage 复活裁决两格** `resume.row_recycling` → `row-contended`(行正在被裁决,
144
+ **窗口自清,过一会儿再发就成**)/ `resume.row_gone` → `row-gone`(行已被终态 GC 收走,
145
+ **等也没用,重开新 agent**)。此前两码双双落进开集兜底 `error`,core 明明给了**两个不同的下一步**,
146
+ 到客户端只剩一句泛泛失败。判定归包、文案归端。
147
+ 🔴 **载体缺口如实登记(2026-08-21 亲验,不在本包侧路补救)**:core 把
148
+ `delegation.concurrency_cap` / `delegation.session_cap` 铸进 Task 工具结果体的 `details.error`,
149
+ 而 core 的 `errorCode` 抬升腿只读 `details.code` → `details.errorKind` **两个拼法**,
150
+ `structuredFrom` 又要求 `details.type` 落在 `CC_DETAIL_TYPES` 里(这条 detail 连 `type` 都没有)
151
+ ⇒ **今天这两码在 wire 上既不在 `tool_end.errorCode` 也不在 `structured`**,只剩模型面文案。
152
+ 故本批**先立词、不落消费分支** —— 按文案反解正是 `engineErrorCodes.ts` 存在的理由要根除的东西
153
+ ([cross-repo-fix-at-source-constitution]:载体缺口在 core,下游不许侧路补救)。已列上游诉求。
154
+ - **🔴 SDK 提货 7.1.0 → 7.2.0(peer 地板同批抬齐)+ `FleetTaskRow` 两新 wire 键投影**:
155
+ sdk 7.2.0 给 `FleetTaskRow` 声明了 `cycleSeq` / `retiredBy`(#261 §2,**server ≥7.25.0 早已在 wire
156
+ 上发**,只是 SDK 类型迟到 ⇒ 本层此前把两键整个丢弃)。抬地板不是「顺手跟版本」:
157
+ `FLEET_TASK_ROW_WIRE_KEY_TUPLE` 与 `keyof FleetTaskRow` 是**编译期双向等值**钉,补键之后在
158
+ <7.2.0 的实装物上整包编译不过。
159
+ 两位现已投进 `FleetTaskView`:`cycleSeq` = 代际号(fresh spawn 就是 cycle 1,**SendMessage 复活即 +1**;
160
+ 同 id 帧更大 ⇒ 复活、更小 ⇒ 前代迟到帧)、`retiredBy` = **非亲报终态的投影者**(在场 = 这条终态是
161
+ 对账腿从 durable run 行读出来的,发布方死了 —— **幽灵行与正常收尾唯一的 wire 判据**;读侧开集)。
162
+ 🔴 **两位都只在场才落键**:`cycleSeq` 缺席 = 「这条行**没有代际概念**」(同步委派子代 / workflow
163
+ agent / 顶层 run 行),**不是第一代** —— 读成 1 会把「首帧迟到」误判成「复活」;`retiredBy` 缺席 =
164
+ 发布方亲报。任何一个补默认值都会把一个诚实缺席翻译成假话。壳渲染契约尚未声明这两键,已登记在
165
+ `scripts/run-fleet-view-keys-test.mjs` 的 `SHELL_UNDECLARED_LEDGER`(壳提货补声明,本表当天删两条)。
166
+
167
+ ### 已知局限(本版新增)
168
+
169
+ - **`engine_notice` 只有投影半场,没有渲染半场** —— 端不接 chrome 臂 `engine_notice` 的话,
170
+ 这类披露在该宿主上**看不见**(不是报错)。cli 渲染腿排在 0.38.0 提货批。
171
+ - **`delegation.*` 两码今天在 wire 上没有机器可读载体**(见件④ ③),词表已立但消费分支候 core 补
172
+ `details.code` 孪生拼法(或抬升腿兼读 `details.error`)后再接。
173
+ - **`engineCapState` 的 `unprobed` 不区分四种成因**(没 kick / 在飞 / 探测失败 / 刚失效)——
174
+ 刻意合并,分开需要引擎没给的信息;要「探测到底跑没跑」得靠 `engineCapsSettled` 的时序而不是读口。
175
+
31
176
  ## 0.37.0(2026-08-19)
32
177
 
33
178
  **#307 双扫台账余量四条(S43/S44/S47/S25)。行为面**四条**:三条是「上游给了本层没接」的补接
@@ -82,7 +227,7 @@
82
227
  `settleByBase`(`engineCapsSettled` 的语义是「等**当前这一次**探测落地」,抽走在途 promise 会让
83
228
  await 方拿到假「已落地」);空串 ⇒ no-op,从没探过也没失效过的 base ⇒ **真 no-op 不留代际条目**;
84
229
  绝不 throw。**per-base**,不连坐别的 base。
85
- 🔴 **代际计数 `genByBase`(codex 对抗复审 [high] 采纳,同批修)**:失效口清了 `inFlight` 之后同
230
+ 🔴 **代际计数 `genByBase`(对抗复审 [high] 采纳,同批修)**:失效口清了 `inFlight` 之后同
86
231
  base 立刻可以再 kick ⇒ **两次探测并发跑在同一份无版本共享态上**。没有代际时两个真后果,都发生在
87
232
  「重启撞上一次慢 capabilities 请求」这个最该正确的时刻 —— ①旧引擎那次**后**落地会把 caps 覆盖回
88
233
  旧引擎的位(失效等于没做);②旧探测的 `finally` 删掉的是**新 run** 的 `inFlight`/`settleByBase`
@@ -155,7 +300,7 @@
155
300
  判死是三条出路里唯一不可逆的,只配给引擎答了且拒了的语义失败。引擎真死时失败尽快显形:下一轮
156
301
  approvals.list 当场失败走既有诚实红。门:`run-hitl-gate-honesty-test.mjs` 新 F6 组(红先绿后 +
157
302
  四变异各自抓红);耗尽错 message 有负控钉「不落 isAlreadyResolvedGateReason 词表」。
158
- ⚠️ **请托 SDK 半场(候黑板)**:decide 的 per-call 面今天无 timeout 旋钮(只有 signal,合流只能
303
+ ⚠️ **请托 SDK 半场(候协作板)**:decide 的 per-call 面今天无 timeout 旋钮(只有 signal,合流只能
159
304
  收短)—— 若 SDK 给 decide 开 per-call timeoutMs(或对 HITL 面单列长缺省),超时类重试环可整段
160
305
  收敛成一发长等待;#316(快速 ack)落地后总窗亦可回收。
161
306
  - **🔴 P0b:Session busy 幽灵 running 的三选卡半场(存活对账 + 已呈现登记)**。病(案卷车2):
@@ -181,7 +326,7 @@
181
326
  [4664] 四个洞(park 态不在 reapStale 射程 / updated_at 误杀窗 / 409 与 poll 口径短暂相左 /
182
327
  LOCAL 无周期腿)在 `ActiveRunBusySignal.activeTaskStatus` 与存活对账腿的注释上逐条引用 ——
183
328
  409 的 `running` 不是绝对真值。
184
- **codex 对抗复审跟修(同批,真病三件)**:[high] 对账读回**终态词**(completed/failed/blocked/
329
+ **对抗复审跟修(同批,真病三件)**:[high] 对账读回**终态词**(completed/failed/blocked/
185
330
  timeout)不再落 not-parked(其默认文案对已终结的 run 说「wait for it to finish」= 永远等不到的
186
331
  假话)—— 专属结局 **`running-settled`**(诚实说 claim 陈旧/释放中 + 重发指引);[medium] 对账
187
332
  期间用户 Esc ⇒ 探测归来后零派臂/零出卡/零登记(零动作现状行收口);[medium 轻形] 登记表 FIFO
@@ -373,7 +518,7 @@
373
518
 
374
519
  ## 0.34.0(2026-08-17)
375
520
 
376
- **#292 P1(cli 黑板 [4285]/[4309] 定谳)——`REQUEST_FIELD_MATRIX` 的 `settings.<resolved>` 行补
521
+ **#292 P1(cli 协作板 [4285]/[4309] 定谳)——`REQUEST_FIELD_MATRIX` 的 `settings.<resolved>` 行补
377
522
  `print` 车道、撤 `gap:true`。行为面**一条**:print 车道的 `buildTaskRequest` 从此会把端给的
378
523
  `input.settings.resolved` 摊开上 wire(端不给值 ⇒ 仍零 stamp,不会凭空出现)。**
379
524
 
@@ -427,7 +572,7 @@
427
572
  留痕(腿名 `subagent-steer`,每 (腿,taskId) 至多一条 warn),绝不回落在飞 run;**没指名行**
428
573
  (既有两参调用)⇒ 才回落在飞 run。⚠️ 端侧收益要**端传了 `childTaskId` 才到达用户**:壳/web/桌面
429
574
  凡有行上下文的 steer 调用点(壳 `REPL.tsx` 的 `onAgentSubmit` 已持 `task.id`)应跟一行把它传下来。
430
- **会话参数同批改二态**(codex 复审 medium):行登记时捕到了会话 ⇒ 用**行的会话**(与行的 runId
575
+ **会话参数同批改二态**(对抗复审 medium):行登记时捕到了会话 ⇒ 用**行的会话**(与行的 runId
431
576
  同源同拍,是唯一自洽的组合;旧形「台账 run + 现势 session」在 session-bound run 上是确定性
432
577
  404,等于修好了 run 定位却仍然 steer 不到);行没有随行会话(`recordBgParentRun` 只写 run,
433
578
  今天 bg 行多数是这一格)⇒ 退现势会话。刻意**不**跟 resume 腿的「没捕到就干脆不带」——本腿遵
@@ -445,14 +590,14 @@
445
590
  `'server_fail_closed' | 'own_root' | 'own_parent' | 'absent_parent'`,按**实际命中的放行臂**铸值
446
591
  (臂序 = 放行判据求值序,多臂同时成立报第一条)。旧签名把四条**强度不同**的臂压成一个「已放行」
447
592
  布尔事实,端因此无法分级处置。
448
- 🔴 **三档强度,不是四档递减**(codex 复审 high 采纳;首版注把中间一格写成「硬证据」是过度声称):
593
+ 🔴 **三档强度,不是四档递减**(对抗复审 high 采纳;首版注把中间一格写成「硬证据」是过度声称):
449
594
  **会话级证明** = `server_fail_closed` / `own_root`;**进程级成员证明** = `own_parent` —— own-run 台账是
450
595
  进程级 `Set`、按会话零分区,`/clear` 或换会话后**旧会话**的 run 仍命中(= 在册局限 **P-13** 在
451
596
  通知面的同一张脸),端**不得**把它读成「属于当前会话」;**非证据** = `absent_parent`(通知没带
452
597
  `parentTaskId`;老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉)。端拿归属做有副作用的事
453
598
  (落库/跨会话搬运/翻别人的卡)时,后两格都应自裁为「未证明当前会话归属」。
454
599
  两参消费方(0.31.0 形)零改动照旧;接入说明见 `docs/INTEGRATION-CLIENTS.md` §6d。
455
- - **#284 尾件 / `parentTaskId` 脏形 fail-closed(行为面收紧,codex 复审第四轮 high)**:
600
+ - **#284 尾件 / `parentTaskId` 脏形 fail-closed(行为面收紧,对抗复审第四轮 high)**:
456
601
  own/foreign 隔离门写成 `typeof === 'string' ∧ length>0 ∧ !isOwnEngineRun(…)` —— 对 number /
457
602
  object / array / boolean 这类脏值**整条不成立**,门不响、外来通知照收(还会被新 evidence 标成
458
603
  `absent_parent`,而那一格的语义是「老引擎**没带**这个键」= 谎报)。fleet 帧从 wire 上来、SDK 只
@@ -480,7 +625,7 @@
480
625
 
481
626
  ## 0.31.0(2026-08-16)
482
627
 
483
- **#242 批 3 fleet belt 对账承重批(design-242 §3 批 3;黑板 [4000] Q2=A / Q3=B 裁定执行)。
628
+ **#242 批 3 fleet belt 对账承重批(design-242 §3 批 3;协作板 [4000] Q2=A / Q3=B 裁定执行)。
484
629
  🔴 BREAKING(行为面翻面两处,见下);公开面 additive 三件。**
485
630
 
486
631
  - 🔴 **BREAKING②/Q3 三腿翻面**:`getBgParentRun(x) ?? activeEngineRunId()` 的无条件回落**全部
@@ -652,7 +797,7 @@
652
797
 
653
798
  - **#244 F4 族D:配置/模型表单源化(A-028.17/.18/.20 三件单源上收 + A-028.19 对账钉,additive,
654
799
  零 BREAKING)**(2026-08-15;公开导出 707 → 724,+17 全 additive;常驻钉 = pure 门 F4D 段 35
655
- checks + F4D-seat-catalog-pin 条件腿 4 checks(红先绿后:壳钉落地前该腿以断言红留证;codex
800
+ checks + F4D-seat-catalog-pin 条件腿 4 checks(红先绿后:壳钉落地前该腿以断言红留证;复审
656
801
  复审两处 [medium] 采纳 —— 腿剥注释锚初始化式防「删钉留注释」假绿、钉补精确等型轴防
657
802
  readonly/any 漂移,后者以 d.ts 变异 readonly 化 → 壳 typecheck 红实证)):
658
803
  - **件1 `model/providerCatalog.ts`(A-028.17)**:46 家 provider 表的**规范折表层** ——
@@ -688,7 +833,7 @@
688
833
  - **#280 移交两件:probeCause 随卡透传 + tail meta 帧发布口(additive,零 BREAKING)**(2026-08-15;
689
834
  红先绿后,常驻钉 = durable-card-display-keys ⑧ 段 10 checks + pure B7 probeCause 八钉 / B6
690
835
  tail-meta 六钉(含 fail-soft 两钉:sink 同步抛错不撕裂 tail / async 拒绝有界观察零
691
- unhandledRejection —— codex 复审 [high] 采纳);壳 #280 围栏 T6 现状锁候本批包透传落地
836
+ unhandledRejection —— 对抗复审 [high] 采纳);壳 #280 围栏 T6 现状锁候本批包透传落地
692
837
  翻红接线):
693
838
  - **件1 `ApprovalCardRequest.probeCause` 透传**(server ≥7.21.0-rc #253 的结构化探针因由,
694
839
  `{code, roots:{shown,total}, further?}`):0.30.3 的两处「帧→卡入参」显式挑键把上游键丢弃 ——
@@ -757,7 +902,7 @@
757
902
  缺省读法留壳)· `RulesFailure` + `classifyRulesFailure`(处置分类,404 两支绝不共用一格)·
758
903
  `listAllPersistedRules` + `RulesFacade`(keyset 翻页收口:显式 limit 200 / 游标失效丢游标
759
904
  重列一次 / 页数硬帽,绝不交半份清单)· `classifySkippedReason`(散文前缀分类)。
760
- ⚠️ 相对 cli 源形收紧两处(codex F2 两轮,各有门回归钉):
905
+ ⚠️ 相对 cli 源形收紧两处(复审 F2 两轮,各有门回归钉):
761
906
  ① `listAllPersistedRules` 的 params **不收调用方 cursor**(类型 `Omit<…,'cursor'>` + 运行期
762
907
  剥除留痕)—— 源形的 `...params` 会把外来 cursor 原样送出,「列全」从中途起步却报完整清单;
763
908
  ② 页体 **fail-closed 窄化** —— 源形把坏形 2xx 页(`{rev:9}` 无 rules 数组 / rev 非有限数 /
@@ -810,7 +955,7 @@
810
955
  镜像脏值的正面证据 ⇒ 不 stand down 放行真分诊;>0 / 抛错 / 读口缺席 ⇒ 保守维持(存量装配零影响)。
811
956
  另导出 `INTERACTIVE_WAY_OUT`(默认出路串单源 —— 宿主 resume 对账腿等失败半场要说同一句真出路,
812
957
  各写一份必漂;覆盖语义照旧走 `ActiveRunSelfHealCopy.wayOut`)。
813
- - **#244 F1 · 复审跟修批**(2026-08-14;同窗 codex 轮一三条 + cli 换装批 G-c 尾竞态,红先绿后
958
+ - **#244 F1 · 复审跟修批**(2026-08-14;同窗 复审 轮一三条 + cli 换装批 G-c 尾竞态,红先绿后
814
959
  G9⑥/⑦ 四断言):① `waitForClaimRelease` 被**窗自己的截止**掐断的探测不再作废上一发完成读数
815
960
  (404/传输错是「回答了不知道」才作废;不区分则 `confirmedHeld` 在窗尾随机翻 false,同输入两跑
816
961
  两种文案);② 假死锁复核读口加 **4s 有界窗**并合流 `deps.signal`(probe 恒不落地时整棵分诊树
@@ -837,7 +982,7 @@
837
982
 
838
983
  ## 0.30.0 — 2026-08-14
839
984
 
840
- > 发车门清账完成:0.30.0 发版扫描 36 条 confirmed 全处置(codex 八轮终轮零新),
985
+ > 发车门清账完成:0.30.0 发版扫描 36 条 confirmed 全处置(复审 八轮终轮零新),
841
986
  > 最后一门 P-30(`findPendingForTask` 门种闸)红先绿后闭环,29/29 门绿后发布。
842
987
 
843
988
  - **P-30:`findPendingForTask` 门种闸**(2026-08-14;**行为面** + 1 新导出;0.30.0 发车前最后一门):
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.37.0
38
+ **Version:** 0.38.1
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
@@ -65,7 +65,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
65
65
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
66
66
  five modules, and the browser bundle really bundles the SDK through (the portability guard would
67
67
  exit 3 rather than quietly mark it external).
68
- - The declared floor is `>=7.1.0`, and it is *witnessed*: the guard checks that an actually
68
+ - The declared floor is `>=7.2.0`, and it is *witnessed*: the guard checks that an actually
69
69
  installed SDK at that line still exports every value-level symbol this package imports and still
70
70
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
71
71
  promise, not a contract.
@@ -249,6 +249,7 @@ public-surface guard checks that last one).
249
249
  | `scripts/run-catalog-loader-gates-test.mjs` | The model-catalog candidate chain (`loadCatalogWithSources`) and the provider device-code seam: offline ⇒ `bundled` with an honest `online.reason`, a good source ⇒ `online` plus a cache write, a second offline run ⇒ `cacheHit`; the three hostile source shapes (malformed JSON, `schemaVersion: 99`, off-domain `http`) each fall through to the bundled table, and an off-allowlist target is **never dialled** — including a `302` to another host, proven by a real loopback server's hit counter staying at zero; a one-byte edit to `catalog.sha256` drops that source while an unavailable sidecar only warns; and the device-code poller's `pending → ok` / `expired` arms run against a real loopback HTTP server with an injected clock |
250
250
  | `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op |
251
251
  | `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
252
+ | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never appear in a published artefact. The guard first proves each pattern still bites on a constructed sample (a screen that cannot fail is worse than none) and honours a per-pattern allow-list for legitimate product vocabulary, so the verdict is "clean surface", not "quiet grep". |
252
253
  | `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T | null`, the `installHost` family returns `T | undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report |
253
254
  | `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
254
255
  | `scripts/run-rules-side-test.mjs` | The persisted-permission-rules lane's shared decision half. The two capability bits are checked as **two independent gates** — a worker can honestly advertise the rules lane while predating the revoke routes, and that shape must *hide* the governance surface rather than render a dead entry. Failure classification is by **disposition, not cause**: the two 404s (route missing vs. dead ticket) never share a bucket, a 503 `rule_import_retry` means *the ticket is still alive* (the opposite handling of a dead one), and a stale-cursor 400 drops the cursor and re-lists from the top exactly once — never resuming a stale keyset, never surfacing a partial governance list, and never paging past the hard cap. The persist-ack reader is **merged into** `readToolApprovalRespondAck`: the three-state verdict (`persisted` / `refused` / `unknown`) is derived only from an ack that passed the package's structural narrowing, and a half-shaped object such as `{rulePersisted: true}` with no `delivery` reads as `unknown` — the pre-merge shell read would have said `persisted`, which is precisely the double-ledger drift this file closes, so that case is pinned in reverse. The local-allow-rule skeleton pins all five narrowings (whole-tool, tool-name match, literal anchor with the escaped-star counter-example, bare interpreter prefix consulted only for Bash, and the canonical dangerous-pattern overlay) **with their refusal strings byte-for-byte** — the cli's 128-assertion suite anchors the same strings, so a one-character edit here changes observable behaviour on three clients — and asserts the parse is a pure function of its input, because the same call backs both "render the option" and "resolve the selected value" |
@@ -254,6 +254,45 @@ const humanInputArm = function* (m) {
254
254
  ...(str(m.eventId) !== undefined ? { eventId: str(m.eventId) } : {}),
255
255
  });
256
256
  };
257
+ /**
258
+ * #310 / #318 件① —— `engine_notice` 引擎结构化通告(server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)。
259
+ *
260
+ * 姿势抄同族的 `human_input`(引擎侧发生了一件事、壳记一笔的非持久帧,归 chrome;**不走 attachment**:
261
+ * 通告要带结构化事实与幂等键,折成一行文案会逼消费端反解 code)。
262
+ *
263
+ * 🔴 **开集:一个码都不判**。这里没有识别表、没有 switch、没有「已知才投」—— `code` 认不认得是
264
+ * **渲染面**的事,投影面只负责把事实原样送到。`detail` 同理**整对象透传**(禁挑键:逐码不同且开集,
265
+ * 挑键 = 新码的事实在本层静默蒸发)。
266
+ * 🔴 **不读能力位**:帧到了就投影(A-022「到帧即服务」)。拿探测位 gate 一条已经到手的事实,只会在
267
+ * 探测未判/失败时把真事实丢掉。
268
+ * 🔴 子流帧(带 `parentToolCallId`)同族让位:不上 leader 面。
269
+ * ⚠️ 第二道 `code` 在场判(投影层已判 malformed):同 `human_input` 的 carrier 二道判,防的是**非投影
270
+ * 口喂进来的帧**(宿主自建管线 / 重放存量转录),不是重复判据。
271
+ */
272
+ const engineNoticeArm = function* (m) {
273
+ if (typeof m.parentToolCallId === 'string')
274
+ return;
275
+ const code = typeof m.code === 'string' ? m.code : '';
276
+ if (code.length === 0)
277
+ return;
278
+ const rawDetail = m.detail;
279
+ const detail = typeof rawDetail === 'object' && rawDetail !== null && !Array.isArray(rawDetail)
280
+ ? rawDetail
281
+ : {};
282
+ yield chrome({
283
+ kind: 'engine_notice',
284
+ laneProof: MAIN,
285
+ code,
286
+ message: typeof m.message === 'string' ? m.message : '',
287
+ detail,
288
+ ...(typeof m.sessionId === 'string' && m.sessionId.length > 0 ? { sessionId: m.sessionId } : {}),
289
+ ...(typeof m.ts === 'number' && Number.isFinite(m.ts) ? { ts: m.ts } : {}),
290
+ ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
291
+ // durable 重放的第二层身份(SDK 从 SSE `id:` stamp 的 per-task 序号)—— 两个键两个命名空间,
292
+ // 见投影层同位的长注。消费端幂等序:eventId > eventSeq > (都缺才退) code+ts。
293
+ ...(typeof m.eventSeq === 'string' && m.eventSeq.length > 0 ? { eventSeq: m.eventSeq } : {}),
294
+ });
295
+ };
257
296
  const promptSuggestionsArm = function* (m) {
258
297
  // T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
259
298
  if (typeof m.parentToolCallId === 'string')
@@ -714,6 +753,7 @@ export const ARMS = new Map([
714
753
  ['steering_injected', steeringInjectedArm],
715
754
  ['workspace_changed', workspaceChangedArm],
716
755
  ['human_input', humanInputArm],
756
+ ['engine_notice', engineNoticeArm],
717
757
  ['prompt_suggestions', promptSuggestionsArm],
718
758
  ['retry_status', retryStatusArm],
719
759
  ]);
package/dist/adapt.d.ts CHANGED
@@ -50,7 +50,7 @@ export interface AdapterLedgerState {
50
50
  /** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
51
51
  export declare const ADAPTER_COVERAGE: {
52
52
  /** 已落码的帧臂。 */
53
- readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "human_input", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
53
+ readonly frames: readonly ["assistant", "user", "stream_event", "turn_usage", "result", "system", "task_notification", "workflow_complete", "diagnostics", "steering_injected", "workspace_changed", "human_input", "engine_notice", "prompt_suggestions", "retry_status", "task_progress", "tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈 + 经 cards.close 铸 tool_result)"];
54
54
  /** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
55
55
  readonly transcriptKinds: readonly ["assistant_text", "assistant_thinking", "assistant_tool_use", "user_tool_result_decision", "user_task_notification", "system_passthrough"];
56
56
  /** B3(0.5.0)新落码的臂/半场 —— 从 todo 移过来的,别再在 todo 里留同名条目。 */
package/dist/adapt.js CHANGED
@@ -33,6 +33,8 @@ export const ADAPTER_COVERAGE = {
33
33
  'workspace_changed',
34
34
  // FIX⑦(2026-08-07):`human_input` 账本帧 —— 投 chrome 记账,**不铸 transcript 行**(不带正文)。
35
35
  'human_input',
36
+ // #318 件①(2026-08-21):`engine_notice` 引擎结构化通告 —— 投 chrome(开集,一个码都不判)。
37
+ 'engine_notice',
36
38
  'prompt_suggestions',
37
39
  'retry_status',
38
40
  'task_progress',
@@ -256,7 +256,7 @@ export type SelfHealOutcome =
256
256
  taskId: string;
257
257
  }
258
258
  /**
259
- * 存活对账腿(codex 对抗复审 [high] 采纳,2026-08-19):wire 409 说这条 run `running`,而出卡前的
259
+ * 存活对账腿(对抗复审 [high] 采纳,2026-08-19):wire 409 说这条 run `running`,而出卡前的
260
260
  * `runs.get` 读回**终态词**({@link CLAIM_RELEASED_STATES})—— run 已终结,claim 是陈旧/释放中的
261
261
  * 投影([4664] 洞③:409 与 poll 口径短暂相左)。三选卡不呈(steer/cancel 对已终结的 run 全是假
262
262
  * affordance),也**不自动重发**(claim 可能尚未真正落地释放)—— 如实说 + 「再发一次」真出路。
@@ -219,7 +219,7 @@ const RUNNING_LIVENESS_RECHECK_TIMEOUT_MS = 4_000;
219
219
  // 走 {@link clearRunningChoiceOffer}。
220
220
  // run 身份变化天然换键(taskId 不同);同 run 状态迁移到 park 态会走别的臂,登记不拦。
221
221
  const runningChoiceDeclined = new Set();
222
- /** 登记表上限(codex 复审轻形采纳):超限 FIFO 驱逐最老条目 —— 驱逐代价 = 卡重弹一次(修前行为)。 */
222
+ /** 登记表上限(对抗复审轻形采纳):超限 FIFO 驱逐最老条目 —— 驱逐代价 = 卡重弹一次(修前行为)。 */
223
223
  const RUNNING_CHOICE_LEDGER_CAP = 512;
224
224
  function runningChoiceDeclineKey(sessionId, taskId) {
225
225
  // NUL 分隔:两段都是外来串,可打印分隔符在「sessionId 里恰有它」时会串键。
@@ -378,7 +378,7 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
378
378
  // [3892]-[3899] 假死锁防御(deps.listOwnedPendingApprovals 头注全案):镜像说有卡 ⇒ 向引擎
379
379
  // 复核一次属主 pending 行数。恰 0 = 镜像脏值的**正面证据**(引擎侧没有任何等这个用户的决定)
380
380
  // ⇒ 放行真分诊;抛错/非 0 ⇒ 保守维持(真卡在屏时 stand down 是对的)。
381
- // 🔴 复核必须**有界且可回收**(codex #244 F1 轮一 [high] + 轮二 [medium]×2):
381
+ // 🔴 复核必须**有界且可回收**(复审 #244 F1 轮一 [high] + 轮二 [medium]×2):
382
382
  // · probe 恒不落地(引擎挂死且读口无自带超时)不许把整棵分诊树挂死 —— 假死锁防御自己变成
383
383
  // 真死锁;窗尽/调用方中止 = 分不出真卡与幽灵,与抛错同臂保守 stand down;
384
384
  // · 合流 signal **真交到 probe 上**(超时/中止掐的是底层请求,不是只掐调用方的 await);
@@ -553,7 +553,7 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
553
553
  }
554
554
  if (ghost)
555
555
  return { kind: 'running-not-found', taskId };
556
- // (codex 复审 [medium] 采纳)对账期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记)都不做 ——
556
+ // (对抗复审 [medium] 采纳)对账期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记)都不做 ——
557
557
  // Esc 之后还弹卡/重开 = 用户说停还在动。零动作现状行收口(与呈卡腿抛错同一条诚实收口)。
558
558
  if (probeCallerAborted())
559
559
  return notParked;
@@ -562,7 +562,7 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
562
562
  return planReviewArm(taskId, busy, deps);
563
563
  if (ASK_PARK_STATES.includes(fresh))
564
564
  return askParkArm(taskId, busy, deps);
565
- // (codex 复审 [high] 采纳)终态词 ⇒ 专属结局:not-parked 的「wait for it to finish」对一条
565
+ // (对抗复审 [high] 采纳)终态词 ⇒ 专属结局:not-parked 的「wait for it to finish」对一条
566
566
  // 已终结的 run 是永远等不到的假话。表外的未知词仍落 not-parked 如实说(不替引擎断言终结)。
567
567
  if (CLAIM_RELEASED_STATES.includes(fresh))
568
568
  return { kind: 'running-settled', taskId, status: fresh };
@@ -590,7 +590,7 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
590
590
  if (choice === 'wait') {
591
591
  if (offerDelivered) {
592
592
  runningChoiceDeclined.add(declineKey);
593
- // (codex 复审 [medium] 轻形采纳)FIFO 有界:长命桌面/多会话宿主不许无界长住。驱逐最老条目的
593
+ // (对抗复审 [medium] 轻形采纳)FIFO 有界:长命桌面/多会话宿主不许无界长住。驱逐最老条目的
594
594
  // 代价 = 那条 run 的卡重弹一次(= 修前行为,方向安全);Set 按插入序迭代,首项即最老。
595
595
  if (runningChoiceDeclined.size > RUNNING_CHOICE_LEDGER_CAP) {
596
596
  const oldest = runningChoiceDeclined.values().next().value;
@@ -961,7 +961,7 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
961
961
  `longer exists). sema did not offer to steer or cancel it: there is nothing left to act on. ` +
962
962
  `Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
963
963
  case 'running-settled':
964
- // codex 复审 [high] 采纳:run 已终结而 claim 仍报 running([4664] 洞③的窗口形)——
964
+ // 对抗复审 [high] 采纳:run 已终结而 claim 仍报 running([4664] 洞③的窗口形)——
965
965
  // 「wait for it to finish」对它是永远等不到的假话;真出路 = 再发一次(claim 落地即跑)。
966
966
  return (`Run ${outcome.taskId} has already finished (status ${outcome.status}), but the engine still ` +
967
967
  `reported it as holding this session — that claim looks stale or mid-release. sema did not offer ` +
@@ -42,6 +42,8 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
42
42
  'prompt_suggestions',
43
43
  'human_input',
44
44
  'turn_usage',
45
+ // #310 / #318 件①:引擎结构化通告的会话面(raw 预分派铸点,见 eventToSdkMessage 顶部)。
46
+ 'engine_notice',
45
47
  ]);
46
48
  /** Wrap neutral content blocks in the CC `assistant` message envelope. */
47
49
  function assistantArm(ctx, content) {
@@ -87,6 +89,24 @@ function assertNeverArm(_ev) {
87
89
  * 旧写法 `if (msg)` 在新返回型上恒真(对象永远 truthy),所以这是**必须点名**的一类改动。
88
90
  */
89
91
  export function eventToSdkMessage(ev, ctx) {
92
+ // ── `engine_notice` raw 预分派(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)──
93
+ //
94
+ // 🔴 **为什么是 raw 预分派而不是一条 `case`**(与 `workflow_complete` / `human_input` 当年同因):
95
+ // 本臂**还没进已发布 SDK 的 `AgentEvent` union**。sdk 仓 `3d6aebc` 确实加了它,但那个 commit
96
+ // **尚未出包** —— 亲验 npm `@sema-agent/sdk@7.2.0`(latest,2026-08-16 发布)的真 tarball:
97
+ // `dist/` 全树零 `engine_notice`(而同批的 `FleetTaskRow.cycleSeq` 在,证明抽检会说话)。
98
+ // 在这样的 union 上写 `case 'engine_notice'` 是编译错,所以先走预分派。
99
+ // 🔴 **这不是「按源码将就接」**(接入文档宪法):消费契约取自 server 的**已发布**接入档
100
+ // (ASSISTANT-WIRE-CONTRACT 附录 D,server 7.36+)与 openapi `Event_engine_notice`,不是抄 sdk src。
101
+ // 档与实装的失真已如实记账(附录 D.3 仍写「起步白名单三码」,而 server main 的白名单已是六码 ——
102
+ // `memory.hold_opened` / `hold_released` / `hold_disposed` 随 core 5.47/5.48 的 `NOTICE_AUDIENCE`
103
+ // 入册)。**本层对此完全免疫**:白名单是 server 的投递判定,本层按开集消费,一个码都不硬编。
104
+ // 🔴 **到期复核(自退休,不靠人记)**:预分派用 `(ev as {type?:unknown})` 形读判别键,**不收窄** `ev`
105
+ // ⇒ 臂一进 union,switch 的 `default` 仍看得见它,B5 穷举断言 `assertNeverArm` **编译期真红**,
106
+ // 逼下一棒把它搬进 switch。搬进去时行为一字不改(下面的投影函数原样复用)。
107
+ if (ev.type === 'engine_notice') {
108
+ return engineNoticeProjection(ev, ctx);
109
+ }
90
110
  switch (ev.type) {
91
111
  // ── `human_input`(core 5.14.0 design/171 / server 7.4.0 SSE,[3017]/[3020])────────────
92
112
  // 🔴 **到期复核已兑现(sdk 6.9.0 提货,2026-08-08)**:本臂此前是 switch **之前**的一条 raw
@@ -548,6 +568,80 @@ export function eventToSdkMessage(ev, ctx) {
548
568
  return dropped('unknown_arm', String(ev.type ?? 'unknown'));
549
569
  }
550
570
  }
571
+ /**
572
+ * `engine_notice` → 中性内部通告臂(#310 / #318 件①,契约 = server `ASSISTANT-WIRE-CONTRACT` 附录 D
573
+ * + openapi `Event_engine_notice`)。
574
+ *
575
+ * ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
576
+ * 引擎的**结构化通告**里,被 server 判为面向**本会话终端用户**的那一小撮,按 `sessionId` 路由到
577
+ * 这条会话的流上。live 与 durable **两腿都有**(server 三条 run 腿都挂了口)⇒ 断连重连的重放里
578
+ * 会**再看到它**,与 `workspace_changed` 同一条**幂等消费**纪律(附录 D.1 逐字)。
579
+ *
580
+ * ── 🔴 开集三条(本函数的全部判据,逐条都是「不许做什么」)────────────────────────────────
581
+ * ① **按 `code` + `detail` 消费,`message` 只作 fallback 展示**。core 明写
582
+ * `memory.session_polluted` 的 message 随 `memoryProvenance` 模式变文 ⇒ 按 message 文本匹配
583
+ * **必碎**(5.41 合流码形退役同教训)。所以本层把三者**分别**上臂,绝不把 detail 折进文案。
584
+ * ② **`code` 是开集,认不得也绝不丢帧**。server 的白名单会随 core 码册增长(起步三码 →
585
+ * core 5.47/5.48 的 hold 三码入册,附录 D.3 的表尚未跟上)。本层因此**一个码都不硬编**:
586
+ * 没有识别表、没有 switch、没有「已知才投」——认不认得是**渲染面**的判断,不是投影面的门。
587
+ * ⇒ 本函数对 `code` 唯一的要求是「非空串」(判别键本身缺席才叫畸形)。
588
+ * ③ **能力位不当两值门**:本臂不读任何 caps、不问「引擎支不支持 engine_notice」。帧到了就投影
589
+ * (A-022「到帧即服务」先例)—— 拿一个探测位去 gate 一条**已经到手的事实**,只会在探测未判/
590
+ * 失败时把真事实丢掉。
591
+ *
592
+ * ── 🔴 消费端纪律(写在臂上,因为三端各写一遍必漂)────────────────────────────────────────
593
+ * · `detail` 已过 server 的 `redactSecrets` + 尺寸 bound(自由文本 1000 字符;深 4 / 键 32 /
594
+ * 数组 32),**仍按外部串处理**(呈现面字符处理走消费方自己的单源)。
595
+ * · `ts` 是 **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。
596
+ * · 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
597
+ * core 顶注):两个数各自读、并列呈现,任何减法都会得出一个**不存在的量**。本层原样透传 detail
598
+ * 正是为了让这条纪律只在渲染面兑现一次,而不是被投影层先算一个差值出来。
599
+ * · **本帧的缺席不代表「没发生」**:非白名单码 / 缺 `sessionId` 的通告 server **如实不投**
600
+ * (宁缺席不串台),全族那一份始终在 server 的结构化日志里(运维面)。所以消费端**绝不许**
601
+ * 从「没收到 engine_notice」反推「记忆姿态正常」。
602
+ *
603
+ * ── 畸形判据(fail-closed 方向)────────────────────────────────────────────────────────────
604
+ * `code` 非串/空串 ⇒ `malformed`(判别键都没有的通告,渲出去只是一行没有主语的噪声,而 dropped
605
+ * 至少会经 `reportDroppedFrame` 留痕)。其余四键**各自**按诚实缺席处理:`message` 非串 ⇒ 空串
606
+ * (fallback 位缺席,渲染面据此走纯 code 呈现)、`detail` 非对象 ⇒ 空对象(**不是**丢帧:通告的
607
+ * 承重物是 code,detail 坏了不该连带把「这件事发生过」一起吞掉)、`sessionId` / `ts` 非法 ⇒ 键不
608
+ * stamp(绝不铸 `0` 这种看起来合法的假读数)。
609
+ */
610
+ function engineNoticeProjection(ev, ctx) {
611
+ const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
612
+ if (code === undefined)
613
+ return dropped('malformed', 'engine_notice');
614
+ const sessionId = typeof ev.sessionId === 'string' && ev.sessionId.length > 0 ? ev.sessionId : undefined;
615
+ const ts = typeof ev.ts === 'number' && Number.isFinite(ev.ts) ? ev.ts : undefined;
616
+ const rawDetail = ev.detail;
617
+ const detail = typeof rawDetail === 'object' && rawDetail !== null && !Array.isArray(rawDetail)
618
+ ? rawDetail
619
+ : {};
620
+ return projected(stamp(ctx, armBody({
621
+ type: 'engine_notice',
622
+ code,
623
+ /** 🔴 fallback 展示位,**不是匹配键**(见本函数顶注 ①)。 */
624
+ message: typeof ev.message === 'string' ? ev.message : '',
625
+ /** 🔴 原样透传(禁挑键):`detail` 逐码不同且是**开集**,白名单挑键 = 新码的事实在本层静默蒸发。 */
626
+ detail,
627
+ ...(sessionId !== undefined ? { sessionId } : {}),
628
+ ...(ts !== undefined ? { ts } : {}),
629
+ // ── 重放身份:两个键、**两个不同的命名空间**,谁都不许顶替谁 ──────────────────────────
630
+ // durable 腿按 `Last-Event-ID` 续读会重放同一条通告 ⇒ 消费端必须能幂等。可用的身份有两层:
631
+ // · `eventId` —— core 铸的稳定事件身份(uuidv7 形)。wire 今天未必带。
632
+ // · `eventSeq` —— **SDK 从 SSE `id:` 字段 stamp 上来的 durable 序号**(= `task_event.seq`,
633
+ // 见 sdk `dist/sse.js` 的 `ev.id = frame.id`;本包的规范访问口就是 `adapter/types.eventSeq`)。
634
+ // 它对「同一条账本行」是稳定的,重放会带同一个值。
635
+ // 🔴 对抗复审 [medium] 采纳(2026-08-21):首版只带 `eventId`,于是 body 不带它时本臂
636
+ // 给消费端留的唯一去重口是 `code+ts` —— 而 `ts` 是**server 观察时刻(ms)**,同一毫秒里同码
637
+ // 的两条不同通告会被折成一条(真事实丢失),跨重连的同一条又可能因为观察时刻不同而重复。
638
+ // 明明有一个稳定序号在手却不带,是本层自己把可靠性降级了。
639
+ // 🔴 **绝不合并成一个键**([same-name-different-meaning-crosses-layers]):`eventId` 是引擎铸的
640
+ // 全局身份,`eventSeq` 是 per-task 的单调序号 —— 塞进同一个字段名会让消费端拿两种语义当一种用。
641
+ ...(typeof ev.eventId === 'string' && ev.eventId.length > 0 ? { eventId: ev.eventId } : {}),
642
+ ...(typeof ev.id === 'string' && ev.id.length > 0 ? { eventSeq: ev.id } : {}),
643
+ })));
644
+ }
551
645
  /**
552
646
  * `human_input` → 中性内部账本臂(见 `eventToSdkMessage` 顶部的 raw 预分派注释)。
553
647
  *
@@ -26,7 +26,7 @@
26
26
  * 能探则探:`GET /v1/capabilities` 的 **`approvals`** 位为 false ⇒ 'not-configured';
27
27
  * caps 面自己不可达 ⇒ 判别缺席,**如实回 'ok'**(绝不编造判别)。
28
28
  *
29
- * 🔴 件 3 考据(读错键的定谳,协作板 [C33]③.3 → server [C34],server 3.13.0 已把辖域写进
29
+ * 🔴 件 3 取证(读错键的定谳,协作板 [C33]③.3 → server [C34],server 3.13.0 已把辖域写进
30
30
  * 代码注与 ARCHITECTURE):本实现此前读 `scheduler`,那是**错的键**。
31
31
  * · `/v1/assistant/*` 的挂载条件是 `checkpointStore`(server `routes/approvals-assistant.ts:67`
32
32
  * 的分支头:`if (deps.checkpointStore && (url.startsWith("/v1/approvals") ||
@@ -169,7 +169,7 @@ export function createBackgroundView(client, opts) {
169
169
  }
170
170
  }
171
171
  /** caps 只需探到一次;失败保持 null 下轮重试(caps 面挂 ≠ assistant 面挂,判别缺席不编造)。
172
- * 🔴 读的是 `approvals` 不是 `scheduler` —— 考据见头注「'not-configured' 判别」节件 3。 */
172
+ * 🔴 读的是 `approvals` 不是 `scheduler` —— 取证见头注「'not-configured' 判别」节件 3。 */
173
173
  async function probeCaps() {
174
174
  if (capsApprovals !== null)
175
175
  return;