@sema-agent/client-core 0.48.0 → 0.50.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 CHANGED
@@ -13,6 +13,11 @@
13
13
  > 不在账上,都红。**发包批的义务** = 往 `FROZEN` **追加**一行(版本号 + 发布 commit + 段 sha256),
14
14
  > **绝不改写既有行** —— 既有行是「那一版当时发了什么」的历史,不是可变配置。
15
15
  >
16
+ > 🔴 **阶段一段头义务(0.48.0 勘误后写死,防第三例)**:发布 commit(阶段一)必须**同批**把
17
+ > `## <version>(未发布)` 段头转成日期形 `## <version>(YYYY-MM-DD)`(0.47.0 先例)。发布后该段
18
+ > 字节即被 ④ 门逐字锁定,漏转**不可回改**、只能永久勘误 —— 0.36.0 与 0.48.0 两条勘误的共同根因
19
+ > 就是这一步没做;此前头注只写了「未发的进 `(未发布)` 段」而没写「发布时转正」,歧义已由本句消除。
20
+ >
16
21
  > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
22
  > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
23
  >
@@ -27,6 +32,181 @@
27
32
  > 都会重新对这个永久冻结的标题判红,退休条件不可能被满足)。门侧核验两件事把这条勘误钉死、
28
33
  > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
34
  > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
35
+ >
36
+ > 🔴 **勘误(0.48.0 发包批,2026-09-01,永久记录 —— 同案第二例,机理同上)**:`## 0.48.0` 段标题的
37
+ > `(未发布)` 是发布批的操作失误 —— 0.48.0 **已经发布**(releasedAt=npm gitHead `4fae01b`,registry
38
+ > 在架),阶段一 commit 应当把段头转正为日期形(0.47.0 先例)而没转,主收货复核也漏了这一格,
39
+ > publish 后才发现。该段字节已被 ④a 逐字锁定在 `4fae01b` 上 ⇒ 标题**不回改、且永远不会被改**;
40
+ > 门侧窄豁免同批登记(`KNOWN_HEADING_ERRATA` `version: '0.48.0', releasedAt: '4fae01b'`),
41
+ > 与本段互钉,均为永久记录。
42
+
43
+ ## 0.50.0(2026-09-03)
44
+
45
+ ### 新增:S-81 selfOrchestration 拒绝的三端公共判定(server ≥7.57.0)
46
+
47
+ server 7.57.0 在**半配置的多租户形态**(`REQUIRE_PRINCIPAL=true` + `SELF_ORCHESTRATION_ENABLED=true`
48
+ + 无中心侧准入解析器)上收窄了三条:`GET /v1/capabilities` 的 `workflows` 由 `true` 变 `false`;
49
+ 同一份 caps 上新增 additive 键 `workflowsGate: { engineCan, denial }`;`POST /v1/tasks`(及同闸的
50
+ stream 提交)带 `selfOrchestration:true` 或 `settings.ultracode:true` ⇒ **501
51
+ `capability.self_orchestration_required`**(去掉这两个键则照常受理;非布尔的 `selfOrchestration`
52
+ 走 400,不是本码)。单用户 worker / 接了解析器的多租户部署行为**逐字不变**。
53
+
54
+ **新增导出**(5 个运行期符号,additive;公面 795 → **800**)
55
+ - `classifySelfOrchestrationRefusal(e: unknown): SelfOrchestrationRefusal | null` —— 判型:
56
+ **只认** `status === 501` ∧ `errorCode` **恰等**该码,其余一律 `null`(永不抛)
57
+ - `stripSelfOrchestrationIntent(req: TaskRequestLike): TaskRequestLike` —— 去掉顶层
58
+ `selfOrchestration` 与 `settings.ultracode` 两个意图键,返回新对象,幂等
59
+ - `projectWorkflowsGate(caps: unknown): WorkflowsGateProjection | undefined` —— caps 上 workflows
60
+ 闸的三位投影(纯函数,永不抛)
61
+ - `SELF_ORCHESTRATION_RETRY_WITHOUT` —— 去键清单单源(`['selfOrchestration', 'settings.ultracode']`)
62
+ - `CAPABILITY_SELF_ORCHESTRATION_REQUIRED` —— 码字面单源(`src/engineErrorCodes.ts`)
63
+ - 类型(type-only,不进运行期基线):`SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` /
64
+ `WorkflowsGateProjection` / `WorkflowsGateUnknownDenial`
65
+
66
+ **三条判定纪律**(端接线前必读,详见 `docs/INTEGRATION-CLIENTS.md` §13)
67
+ 1. **重发判据是合取,且不许放宽成前缀判**:该码是**复用码** —— 与它同前缀的兄弟(别的能力位
68
+ 没接线)去掉这两个键**也不会**变成可受理。无码的 501 判不出 ⇒ `null`,绝不挑一个猜。
69
+ 2. **去键只减两处**:`settings` 下别的子键与 `deferTools` 一个都不动(连坐删 = 用一次重试静默改掉
70
+ 用户的其它设置);只有真删过 `ultracode` 且 `settings` 剩空才整键收口。additive 未知键全保。
71
+ 3. **caps 的缺席 ≠ 关着,认不得的 `denial` ≠ 没有拒绝**:`workflows` 读不出 ⇒ 整只 `undefined`
72
+ (端零渲染);闸缺席 ⇒ `engineCan: undefined`;认不得的 `denial` ⇒ `{ unknown: … }`,
73
+ **绝不**折成 `null`(折了就等于向用户断言「没有任何拒绝」)。
74
+
75
+ **端的义务**(本包不做):重发**只一次**、第二次仍 501 按普通失败呈现;重发必须留一行诚实披露
76
+ (用户按的是「用 ultracode 跑」,实际跑的是不带编排的那一发);端若在用幂等键,重发必须换新键
77
+ (body 变了,复用旧键会让 server 回放那条 501)。措辞全归端 —— 本件零文案。
78
+
79
+ **异源对抗复审采纳两条**(均已同批修 + 同批补门,两条变异各自实测翻红):
80
+ - `SELF_ORCHESTRATION_RETRY_WITHOUT` 改为 **`Object.freeze`**(`as const` 只管编译期;判决的
81
+ `retryWithout` 与它是同一只引用 ⇒ 任一消费者 `splice` 一下就污染此后每一次判决,端照它去键
82
+ 会删掉别的字段)。
83
+ - 分类器**只在 `501 ∧ 恰码` 两条判据通过之后才读 `denial`**(它不是判据,是判决上的附加位;
84
+ 此前无条件先读 = 每一个根本不匹配的抛出物都被跑一次它的 getter)。同批把射程分界写清:
85
+ 「getter 零执行」只对读 wire JSON 的 `projectWorkflowsGate` 成立,读抛出物的分类器刻意走普通
86
+ 属性读取(抛出物可能是 SDK `APIError` 类实例,判据位坐在原型上),那一侧只承诺「不抛」。
87
+
88
+ **异源对抗复审第二轮再采纳一条**(档面缺陷,同批修 + 同批补门,变异实测翻红):
89
+ - §13b 首版只给了 `try { await submit(req) } catch { … }` 一种接法,而实装 SDK 的
90
+ `tasks.stream()` 是 **async generator** —— 调用它**不发请求**,POST 在 `streamRaw` 里、要等
91
+ **第一次迭代**才跑。照那一段接线的流式端永远进不了 catch ⇒ 分类器不运行、去键重发整条失效,
92
+ 半配置部署上用户仍然直接看到 501。现已拆成两段范式(同步腿 / 流腿),并说明 generator 用过
93
+ 不能重来、去键后要建新的一条;常驻门用**真 `TasksResource`** + 假传输层离线钉住两条腿的差异
94
+ 并与档对账。
95
+ - 同批订正 §13c 的一处自相矛盾:四处不可信读取「accessor 一律当缺席」这句概括与上面的表冲突 ——
96
+ `gate.denial` 的 accessor 落 `{ unknown: '' }` 而**不是** `null`(键不在场才是「没有拒绝」,
97
+ 在场却读不出是「拒了但读不出」),端照概括实现正好会犯本节要防的那个错。
98
+
99
+ **异源对抗复审第三轮再采纳三条**(同批修 + 同批补门,三条变异各自实测翻红):
100
+ - **类型面**:`stripSelfOrchestrationIntent` 补 **SDK `TaskRequest` 重载** —— 只留宽形
101
+ (`Record<string, unknown>`)会**擦掉**调用方的类型,一份 `TaskRequest` 进去出来就不再可赋回
102
+ `TaskRequest`,于是 §13b 那句「去键之后直接重发」在 TypeScript 上编不过、端只能靠 `as` 强转。
103
+ 本函数删的两位在 `TaskRequest` 里都是**可选位**,所以窄重载是类型面成立的事实而不是宽容。
104
+ - **取消语义**:§13b 的范式补 `signal` 透传与重发前的 `signal?.throwIfAborted()` —— 首发 501 与
105
+ 去键重发之间隔着一次分类 + 一次写披露,人完全可能就在这个窗口里按下停止;少了中断闸,端会在
106
+ 用户已经喊停之后再发一条会跑工具、会烧 token 的 run,还写一行「已按不带编排的方式继续」。
107
+ - **门的假绿面**:G5 的假传输层原先直接从 `openStream` 抛一只自捏的 `{status, errorCode}`,把 SDK
108
+ 真正那一跳(`openStream` 回非 OK `Response` → `errorFrom` 铸 typed error)整个绕过去了。现改为
109
+ 回**真的 501 `Response`** 并接 SDK 自己的 `errorFromResponse`,钉调用序;同时把 §13b 两段范式
110
+ **照着实现跑一遍**(恰重发一次 / 第二发真去键 / 第二个 501 直接外溢 / 中断后零重发零披露),
111
+ 替掉原先的关键词计数式对账。
112
+
113
+ **异源对抗复审第四轮再采纳三条**(同批修 + 同批补门,两条变异各自实测翻红):
114
+ - **[high] 干净 EOF 不等于跑完**:SDK 的 `tasks.stream()` 只在见到 `done` / `failed` 时 `return`,
115
+ 流被中途干净截断(或一帧都没产出)时 `for await` **同样正常结束** ⇒ §13b ② 的 `runOnce` 会把一次
116
+ **被腰斩**的 live task 静默当成功。范式补 `sawTerminal` 判并在缺终帧时响亮抛;门的「成功」假体改发
117
+ 真终帧、另加「有帧无终帧 + 干净 EOF」负控。这一条与去键重发**正交**(对每一条流都成立),
118
+ 同批写进 §13d 端必读第 5 条。
119
+ - §13b 的 `{ transientOk: true, signal }` 在开了 `exactOptionalPropertyTypes` 的端上 **TS2379**
120
+ (SDK 声明的是 `signal?: AbortSignal`)⇒ 两处改条件展开;并让 G6c 把**档里那两段范式原样**送去
121
+ 真 tsc 编(此前 G6 只编去键重载的赋值,档里编不过的范式照样能留着,而端是照档抄的)。
122
+ - 门的中断用例原先绕开共享 `drive` 另写了一段必然先 abort 再 `throwIfAborted` 的局部流程,
123
+ 也没有零披露断言 ⇒ 改走同一条真 `drive`(经测试钩在分类后触发 abort),并把披露账改成
124
+ **调用方持有**的 out-param 使抛出路径上也观察得到,同时断言 `signal` 真的透传进了 generator。
125
+
126
+ **异源对抗复审第五轮再采纳四条**(同批修 + 同批补门,三条变异各自实测翻红):
127
+ - **[high] 取消原因保真**:`readSseFrames` 见到 `signal.aborted` 会**正常** return ⇒ 上一轮新加的
128
+ 终帧判会把一次**用户主动取消**改写成 `Error: stream ended without a terminal frame`
129
+ (实测复现)。靠 `AbortError` 抑制报错 / 决定要不要重试的端会把取消渲成故障,甚至照着「故障」
130
+ 再提交一次。⇒ 范式改成 `signal?.throwIfAborted()` **先于**缺终帧抛,门加「帧到手后在 handler 里
131
+ 取消」的真 SDK 用例(终帧晚到的 `ReadableStream` 假体)并在档面钉住两条抛的先后顺序。
132
+ - 门的 `runOnce` 用 `frames.push(ev)` 顶替了档里的 `handle(ev)` ⇒「断言 handler 收到终帧」是假的;
133
+ `drive` 的 rethrow 臂也是 `return` 而不是 `throw`,与档不同形。⇒ 改跑档同形的 `handle` /
134
+ `transcript.note` 真 spy,`drive` 改成逐字同形(认不得就抛),观察挪进 harness 侧薄封装;
135
+ 披露账补**引用同一性**正面断言(否则中断腿那条「零披露」是恒真断言)。
136
+ - G6c 把两段范式**合并**成一个虚拟模块编译,于是第二段自身缺 import 搭上了第一段的便车 ⇒
137
+ 「两段范式原样编得过」不成立,端只抄第二段会 TS2304。改为**逐块独立**编译、前缀只声明宿主提供位
138
+ (一个库导出都不注入),两段范式各自补全 import,并加「去掉自带 import 必须红」的第二条负控。
139
+ - §13d 标题写「四条」而实际已列五条 ⇒ 标题订正,并把标题计数与真正列出的条数做成门里的闭合检查。
140
+
141
+ **异源对抗复审第六轮再采纳五条**(同批修 + 同批补门,三条变异各自实测翻红):
142
+ - **[high] 终帧优先于取消**:上一轮把取消判放在缺终帧抛之前,但它是**无条件**的 —— 取消恰好落在
143
+ **终帧交付之后**时(实测:在 `handle(done)` 里 abort),一条已经有结局、可能已落副作用的 run 会被
144
+ 报成取消,而人多半会再跑一遍 ⇒ 重复执行。⇒ 取消判与截断判都关进 `if (!sawTerminal)`,
145
+ 门加 `handle(done)` / `handle(failed)` 内 abort 的两向回归。
146
+ - 门的披露路径没有跑档里那句 `transcript.note(disclose(...))`(harness 直接 push 数组)⇒ 删掉档里的
147
+ `disclose` 或换成错文案都不会红。改成两个真 spy 并让 `disclose` 铸哨兵串,断言 `note` 收到的就是它。
148
+ - 顺序判锚在**整节**上,而整节里第一处 `throwIfAborted()` 是**同步腿**的中断闸 ⇒ 把流腿改成反序仍绿。
149
+ 改为先切出流腿 `runOnce` 的函数体再比位置,并加反序正控。
150
+ - G6c 的「去掉自带 import 必须红」负控**恒红** —— 红的是前缀自己(`Parameters<typeof buildTaskRequest>[0]`),
151
+ 证不出正文缺 import。前缀改用内联 import 类型与范式解耦,负控加**归因**(诊断必须点名正文里的库符号)
152
+ 与「前缀单独编零诊断」前提;提取范围同批扩到 **§13 全节**(§13c 那段端也会单独抄走)。
153
+ - §0a 未发导出说明写「三个 type-only 形」而实际列了四个 ⇒ 订正并把这个计数也纳入门里的闭合检查。
154
+
155
+ **异源对抗复审第七轮再采纳三条**(同批修 + 同批补门,两条变异各自实测翻红):
156
+ - **[high] 每帧取消闸**:一次 SSE 读取会缓冲**多帧**,SDK 连着把它们 yield 出来(真实时序,不属
157
+ §12e 排除族)。只在整个 `for await` 结束后查 signal ⇒ 人已经喊停,仍会消费同一 chunk 里的后续帧 ——
158
+ 实测:同 chunk `turn_start` → `done`,在 `handle(turn_start)` 里 abort 仍吃掉 `done` 并**报成功**;
159
+ 后一帧若是坏 JSON,取消还会被一条 `SyntaxError` 盖掉。⇒ 非终帧之后立刻 `throwIfAborted()`,
160
+ 终帧那一支先 `break`(「终局优先」在这里也成立)。
161
+ - 门的行为矩阵此前跑的是**脚本内手抄的流腿副本**,同步腿更是只验了「首发 501 可分类」⇒ 新增 **G7**:
162
+ 把 §13b 两段范式**抽出来转译成可执行模块**(库 import 重写到本仓 `dist`)注入宿主位后**真跑**,
163
+ 两条腿各过一遍完整行为矩阵,披露按 `disclose` 铸的哨兵串与档里那一句机械对账。
164
+ - 顺序判此前逐字符切函数体 + `indexOf` 找表达式 ⇒ 注释/字符串里的同名语句能骗过它(实测反例:注释里
165
+ 按正确顺序写诱饵、真代码反序,断言照绿)。改成**锚 TS AST**,并带「注释诱饵 + 真语句反序」负控。
166
+
167
+ **常驻门**:`scripts/run-self-orchestration-denial-test.mjs`(207 checks;条数下限零松量)。
168
+ **棘轮**:`public-export-baseline` 795→800;typeshape `unknownExport` 262→264(两处边界读口入参,
169
+ 逐条登记);portability index 闭包 140→141。
170
+
171
+ ---
172
+
173
+ ## 0.49.0(2026-09-03)
174
+
175
+ ### 新增:崩溃收敛的孤儿审批读面(L-38;server ≥7.55.0)
176
+
177
+ `GET /v1/approvals` 的 additive 键 `crashConverged` —— local 引擎在人还挂在审批门上时崩掉,
178
+ 那些孤儿 ask 被 server 重启后收敛成 DENIED 同码。本版把「上一条命留下了什么」做成三端公共读面。
179
+
180
+ **新增导出**(1 个运行期符号,additive;公面 794 → **795**)
181
+ - `projectCrashConverged(env: unknown): CrashConvergedProjection | undefined` —— 纯投影,零 IO / 零文案 / 零 module 级状态
182
+ - 类型(type-only,不进运行期基线):`CrashConvergedRow` / `CrashConvergedOrphanState` /
183
+ `ApprovalsListEnvelope` / `CrashConvergedProjection`
184
+
185
+ **类型面 additive 放宽**
186
+ - `ApprovalsResourceLike.list()` 返回位:匿名 `{ pending }` → 具名 `ApprovalsListEnvelope`
187
+ (`pending` + additive `livePending?` / `crashConverged?`)。老形 `{ pending }` 的 mock 与真
188
+ `AgentClient.approvals` 都仍可赋值,`hitlBridge` / `approvalsFeed` 两处 `.pending` 消费点零改动。
189
+ 入参收 `unknown` 而非「带可选 `crashConverged` 的对象」:后者是 TS 弱类型,SDK 自己的
190
+ `list()` 回体与它无共同属性 ⇒ TS2559。
191
+
192
+ **三条判定纪律**(端接线前必读,详见 `docs/INTEGRATION-CLIENTS.md` §12)
193
+ 1. **缺席 ≠ 空数组**:键缺席 / 载体不是数组 / 载体读不出 / 行数超上限 ⇒ `undefined`(端零渲染,
194
+ 绝不渲「0 个」);空数组 ⇒ 在场的零计数对象(server 明说「一条都没有」)。
195
+ 2. **进 resumeSafe 是五项合取**:`orphanState==='pending'` ∧ `resumeSafe===true` ∧
196
+ `originalDecision` 缺席 ∧ `decidedAtMs` 缺席 ∧ 行不带 accessor / 自定义原型。
197
+ decided 臂与一切说不清的组合恒落 `needsHuman`(**落桶不丢**)。
198
+ 3. **坏行按类型面丢弃并计数**,`dropped` 与 `total` 分账、坏行不连坐;退化但合型的值
199
+ (`ts:NaN` / 空 `toolName`)不丢 —— 唯一越过类型面的严格是行身份 `approvalId` 非空。
200
+
201
+ **caveat**:`resumeSafe` 是以账本完整为前提的**缺省值不是铁证**,文案只许写「按记录看可以重跑」;
202
+ `crashConverged` 键在场 ≠ 流内协议上场,别当能力位读。
203
+
204
+ **新增常驻门**:`scripts/run-crash-converged-projection-test.mjs`(118 checks)。
205
+
206
+ **已知边界**(§12e 成文):真供给 = server JSON 经 `JSON.parse`。被中间层合成的非 JSON 载荷 /
207
+ 敌意 `Proxy` / 原型注射不在射程内 —— 只承诺不抛、不同步阻塞、绝不产出「可安全重跑」判决。
208
+
209
+ **已知局限**:壳侧接点(`--resume` 后的一行披露)不在本版,cli 下一批认领。
30
210
 
31
211
  ## 0.48.0(未发布)
32
212
 
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.48.0
38
+ **Version:** 0.50.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
@@ -251,6 +251,8 @@ public-surface guard checks that last one).
251
251
  | `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 |
252
252
  | `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) |
253
253
  | `scripts/run-session-memory-status-test.mjs` | The session **memory-status** read face (S-53): the two judgements three clients would otherwise each get wrong. First, *same status, different code* — this route's 404 carries two unrelated meanings (`not_found.session` = unknown or non-owned session; `not_found.route` = a pre-7.53 server that has no such route at all), so dispatching on the **status** would report "your deployment lacks this surface" as "your session does not exist". The verdict is anchored on `errorCode`, the two 404s are pinned to **different** verdicts, and — the load-bearing negative control — a 404 carrying **no** code falls to `failed` rather than guessing either way, since a wrong guess in either direction is a false statement a user would act on. 501 is allowed a codeless fallback because both of its arms mean the same thing here, and `capability.*` stays split from `feature.*` because those two share a status while their dispositions are opposite. Second, *absence means something different per key*: `optOutSource` and `lastCaptureAt` are legitimately absent on a **healthy** session (a zero-history session really is `{captureOptedOut:false, committedCount:0, foldedCount:0}` with no degradation at all), so reading absence as "off/none/0" asserts something unprovable. Two combined readers are pinned: capture opt-out is read from **both** its keys (a record-store fault yields `indeterminate`, never `active` — the difference between "your conversation is being remembered" and "nobody knows"), and last-capture is a **three-state** read whose discriminator is the *other* key, because `lastCaptureAt`'s absence alone covers both "ledger unreadable" and "genuinely no contributions" and therefore decides nothing; the two shapes are pinned to different verdicts so a single-key read turns red. The thin wrapper is the only IO: it never throws, drops malformed keys to absence rather than trusting them (an unreadable value must answer "don't know", never render as truth), refuses to spend a request on an empty `sessionId`, and passes `signal` through untouched |
254
+ | `scripts/run-crash-converged-projection-test.mjs` | The `crashConverged` read face on `GET /v1/approvals` (L-38): what the *previous life* of a crashed local engine left behind, projected for every client. Three judgements are pinned. First, **absence is not an empty list** — a missing key (an older server, deps not present, or a carrier that is not an array at all) returns `undefined`, and the client renders nothing; an empty array returns a present zero-count object, which is the server actually saying "none". Folding the first into `{total:0}` would have the client assert "nothing was left behind" on a surface a person uses to decide whether it is safe to re-run something — the worst possible direction for a false statement — so the two cases are pinned to different **return shapes** and a test asserts the two verdicts are unequal. Second, bucketing is a **four-term conjunction**: `orphanState === 'pending'` *and* `resumeSafe === true` *and* both approval-evidence keys (`originalDecision`, `decidedAtMs`) absent. A fifth term rejects any row carrying an **accessor**, and accessors are never invoked at all — reading one means synchronously running someone else's code, and `catch` catches throwing, not *never returning*, so a looping getter would pin the startup thread forever (the row cap does nothing against that shape). The same rule covers the three untrusted reads outside the row as well — the envelope's `crashConverged` key, the carrier's `length`, and every numeric index are read as own property *descriptors* and only data descriptors are used, so accessors and prototype entries read as absent and are never invoked. Such a key is treated as absent: if it was a required field the row is counted as dropped, if it was optional or additive the row survives without it. That also closes the ordering attack, since spreading runs getters in property order and an earlier one could `delete` the approval evidence before it is ever copied (measured before the fix: such a row reached the resume-safe bucket), and the check therefore moves ahead of the read, onto the property descriptors — from which the snapshot is then built directly, because checking descriptors and *then* spreading is two independent observations of the same row, and a non-throwing proxy can make the two `ownKeys` calls disagree (first showing `originalDecision: 'approve'` so the row reads as plain data, then omitting that configurable key so the snapshot loses the evidence; measured before the fix: the dangerous row reached the resume-safe bucket after exactly two enumerations, and after it, one). Keys are written with `Object.defineProperty` rather than plain assignment, because `'__proto__'` is a legal own enumerable key and `o['__proto__'] = x` does not store a value — it calls the prototype setter, letting a row whose own properties are all plain data (so the accessor gate never fires) inject a prototype whose `sessionId` getter deletes the approval evidence from the snapshot during validation; `defineProperty` fires no setter, so the key survives as ordinary additive data and the snapshot keeps `Object.prototype`. A row that simply arrives with a custom prototype is treated the same way, since the snapshot only enumerates own properties: approval evidence sitting on the prototype would never reach it, and a perfectly ordinary object with no proxy and no accessors could otherwise be called safe to re-run — real bodies come from `JSON.parse` and always carry `Object.prototype`, so nothing genuine trips it). Validation itself runs on a **null-prototype** dictionary and the bucketing verdict is carried out of that same pass rather than re-read from the delivered row, because every property lookup on an ordinary `{}` reaches `Object.prototype`: a polluted `sessionId` getter there would delete the approval evidence from the snapshot mid-validation and send the row to the safe bucket (measured before the fix). The row handed to the client is still an ordinary object — the null prototype is an implementation detail of the check, not of the value) — real JSON bodies are all data properties, so only a middle-layer-synthesised payload ever trips it, and it too lands in the human bucket rather than being dropped. The `decided` arm means the human had already approved and side effects may be half-landed, so it always goes to the human bucket, as does `resumeSafe === false` and — the last two terms — any row whose own fields contradict each other, since `pending` claims nothing ran while that evidence says somebody pressed approve. Deciding "not safe" costs one extra question (recoverable); deciding "safe" wrongly has somebody re-run work that already partly happened (not). A 2x2 truth table pins that exactly one cell is resume-safe, so reading either key alone turns red, and the contradictory rows are routed to the human bucket rather than dropped — they are real orphans, and the ones most worth showing. Third, unreadable rows are **dropped and counted**, never thrown and never passed through: the product is declared as `CrashConvergedRow`, so letting a row missing a required field — or carrying one of the wrong type — past would be a lie at the type level, and the closed literal discriminators (`decision` / `cause` / `orphanState`) decide family membership rather than being an open vocabulary. The measuring stick stops at the **type** floor, though: degenerate-but-well-typed values (`ts: NaN`, an empty `toolName`) are kept, because swallowing a real orphan over a decorative field is the worse direction, and the one deliberate exception is `approvalId`, which must be non-empty to be a row identity at all. `dropped` is kept separate from `total` so unreadable rows never inflate "N approvals were affected"; each row is a **one-shot snapshot** — every own enumerable key is read exactly once, and validation, bucketing and the handed-back value all read that same snapshot, so additive upstream keys survive while a **non-idempotent** getter (one that never throws, just answers differently on a second read) can no longer erase the approval evidence between the check and the bucketing (measured before the fix: such a row landed in the resume-safe bucket while its checked value was `"approve"`). Hostile carriers are counted rather than allowed to reject: **every** touch of the carrier is guarded — envelope property reads, `Array.isArray` itself (it throws on a revoked proxy), the `length` read, each indexed read and each row's property reads — and a traversal that dies halfway returns absence rather than a half-counted total. A row that cannot be read never takes the batch with it: its own shape check is inside its own guard, so one revoked-proxy row costs a `dropped` tick rather than collapsing the whole projection to absence — which a client would have read as "this deployment does not offer the surface". Traversal goes by **numeric index, never the carrier's own iterator protocol**, because `for...of` hands the carrier the question of which rows exist: an array carrying an overridden `Symbol.iterator` can yield nothing (measured before the fix: a real orphan became `{total:0}`, which a client reads as "the server said there are none") or swap a dangerous `decided` row for a safe-looking one (measured: `fake-safe` was returned in place of `real-danger`). Row count is capped at 100000 and the cap is checked **before** the walk: requiring only a non-negative integer `length` does not stop a proxy trap reporting a billion, and this surface runs on the startup / `--resume` path, where a synchronous spin freezes the thread (measured before the cap: twenty million rows took 18.3 seconds and twenty million index reads; a billion does not come back). The honest boundary is stated rather than overclaimed — a proxy can still lie in its `length` or index traps, which is the same thing as a host injecting a lying transport — and the widening of `ApprovalsResourceLike.list()` is proven **additive** by really running tsc over a legacy `{ pending }` mock *and* over the real `AgentClient` path — the projector takes `unknown` precisely because a parameter shaped as "an object with an optional `crashConverged`" is a TypeScript weak type that the installed SDK's own `list()` return shape shares no property with, which only a real-client compile would have caught — with a known-red control so a clean run means the checker spoke |
255
+ | `scripts/run-self-orchestration-denial-test.mjs` | The three judgements behind a **denied self-orchestration request** (server 7.57.0), each of which all three clients would otherwise get wrong on their own. First, whether to retry at all is a **conjunction that may not be loosened**: HTTP 501 *and* an `errorCode` that is **exactly** `capability.self_orchestration_required`. That code shares its shape with every other `capability.*` 501, so dispatching on the prefix would drag "some other capability is not wired up" into the retry arm — those requests do not become acceptable once the two keys are gone, so the client would spend a request and then tell the user the wrong reason. Negative controls cover all four directions: a sibling `capability.*` code, a truncated or suffixed variant of the right one, a codeless 501 (it decides nothing, so it decides nothing — no guessing), and the right code under 500 / 400 / 503 or a string `"501"`. The classifier reads structurally rather than by `instanceof` (a host may inject its own transport; across realms or duplicate SDK instances an understandable error would read as unreadable), so a class instance, a bare `{status, errorCode}` literal and an error carrying those fields on its **prototype** all reach the same verdict — and a hostile proxy or a throwing getter yields `null` instead of throwing, because this classifier runs inside a `catch` block where anything it throws escapes the caller's own guard. Second, removing the intent is a **structural** operation, not wording: `selfOrchestration` sits at the top level while `ultracode` sits under `settings` — two different stamping legs — and a client hand-writing `delete` will miss the second one, which costs the user the same failure twice. The single stripper is pinned to touch exactly those two: other `settings` sub-keys and their values survive byte for byte, `deferTools` is left alone (pulling `Workflow` out would be a behaviour change, not a removal of intent), additive unknown keys survive at both levels, the input object is never mutated, `settings` is only dropped entirely when `ultracode` was really there and nothing else remains (an already-empty one is left as is), a non-object `settings` is not touched at all, an `ultracode` that only exists on the prototype does not count, and the whole thing is idempotent. The end-to-end leg runs a real `buildTaskRequest` product through it and asserts the stripped body still passes the registration gate key by key. Third, on the capabilities body, **absence is not "switched off"**: a pre-7.57 server has no `workflowsGate` key at all, so reading absence as "the engine says no" asserts something the server never said, and the mirror-image disease is folding an **unrecognised** `denial` into `null`, which would have the client render "nothing was denied" when the truth is "denied, for a reason I do not recognise". Five shapes are pinned — caps unreadable, gate absent, closed-set member, unknown value, accessor — with the unknown arm carrying the raw token (or an empty one when the value is not even a string) and never collapsing to `null`. All four untrusted reads go through own **data descriptors** only, and the guard pins the getter invocation count at zero, since `catch` catches throwing but not *never returning*; a descriptor trap that throws and a revoked proxy both yield honest absence rather than an exception — though *what* absence means differs by field, and the guard pins that split rather than a blanket rule: an accessor on `workflows`, `workflowsGate` or `engineCan` reads as absent, while an accessor on `denial` reads as `{unknown:''}`, because a key that is **not there** is the gate saying "nothing was denied" whereas a key that is there but cannot be read is "denied, and I could not read why" — folding the second into the first is exactly the false statement this face exists to prevent. Two further pins came out of an adversarial review. The exported retry list is **frozen at runtime**, not merely `as const`: the verdict hands out that same reference, so any consumer splicing it once would poison every later verdict in the process — the guard asserts `Object.isFrozen`, that four different mutation attempts leave it byte-identical, and that a verdict issued *after* those attempts still carries the original two entries. And the classifier reads `denial` only **after** both criteria have passed, since it is not a criterion but an extra field on the verdict: the guard pins the getter invocation count at zero for any error that does not match and at most one for an error that does. The scope line is drawn explicitly rather than overclaimed — "no getter ever runs" holds for `projectWorkflowsGate`, which reads **wire JSON** where every field is an own data property by definition, but not for the classifier, which reads a **thrown value** that may well be an SDK `APIError` class instance carrying `status` and `errorCode` on its prototype; insisting on own data descriptors there would report a perfectly readable error as unreadable, so that side promises only that it never throws. A final pin covers the **integration document's own worked example** rather than the library: the shipped SDK's `tasks.stream()` is an `async` generator, so calling it issues no request at all — the POST happens inside `streamRaw` on the first iteration, and a `try` wrapped around the `stream(...)` call itself can never catch the 501. A client following a submit-shaped recipe on the streaming leg would never run the classifier, and the whole strip-and-retry path would silently do nothing. The guard drives the **real** `TasksResource` against a fake transport, offline, and pins both halves: the synchronous leg is in flight the moment it is called, the streaming leg has issued zero requests after the call and raises on the first `next()` — and it does so through the **real** error path, with `openStream` returning an actual 501 `Response` that the SDK's own `errorFromResponse` turns into the typed error, pinning the `openStream`→`errorFrom` call order so a transport that stops minting `errorCode` cannot pass. The documented recipe is then **executed** rather than keyword-counted: exactly one retry, a second body that really lost both keys while every other setting survives byte for byte, the caller's own request object left untouched, one disclosure and only one, a second 501 propagating with the request count still at two, and — after the first 501 — an abort leaving the count at one with nothing disclosed. A last leg is type-level: `stripSelfOrchestrationIntent` carries an SDK `TaskRequest` overload, because the wide `Record<string, unknown>` form erases the caller's type and the document's "strip and resubmit" line would not compile without an unsafe cast; a real tsc run over a virtual file proves both the narrow and the wide path, with a known-red control — and it compiles the document's two recipes **verbatim**, extracted from the section itself, because a recipe that does not compile is a recipe that was never given: `{ transientOk: true, signal }` is a TS2379 under `exactOptionalPropertyTypes`, which no amount of prose review had caught. The last thing pinned is the one that would have been quietest of all: the SDK's `stream()` returns only on a `done` or `failed` frame, so a stream truncated mid-run — or yielding nothing at all — ends the `for await` just as normally as a completed one. The documented `runOnce` therefore tracks whether it ever saw a terminal frame and raises when it did not, the guard's success fixture emits a real terminal and asserts the handler received it, and a truncated-stream control asserts that shape is reported as a failure with no retry and nothing disclosed. That terminal-frame rule then needed one more turn of its own: the underlying reader returns *normally* when the signal is aborted, so the check as first written rewrote a user's cancellation into a generic stream fault — a client keying off `AbortError` to suppress the error would instead have shown a failure, or resubmitted. Cancellation is therefore checked first, a real-SDK case aborts from inside the handler and asserts the original `AbortError` survives with no retry and nothing disclosed, and the document is checked for that ordering. The harness runs the documented `handle` and `transcript.note` as real spies rather than pushing frames itself, the drive loop rethrows exactly as the document does, and the disclosure ledger is proven to be the caller's own array by a positive identity assertion — without which the cancellation leg's "nothing disclosed" would have been vacuously true. Each recipe is compiled **on its own**, with a preamble that declares only what a host supplies and injects no library symbol, since compiling them together let the second one borrow the first one's imports, and the preamble's own types are decoupled from what the recipes import so the "remove the imports and it must fail" control fails for the right reason — which is checked by attribution, not merely by redness. Ordering is the last thing to get right: the cancellation check must come before the truncation error but **both** must sit behind the terminal-frame test, because a cancellation that lands after the run already reported `done` would otherwise overwrite a real outcome — one that may have already had effects — with "cancelled", and a person reading that will run it again. Aborting from inside `handle(done)` and `handle(failed)` are both pinned to still report success, and the ordering assertion is anchored inside the streaming `runOnce` body rather than the section, since the section's first `throwIfAborted` belongs to the synchronous recipe and would have made a reversed streaming recipe pass — and that ordering check is now anchored on the TypeScript AST rather than on text, since a comment reproducing the two statements in the right order let a genuinely reversed body pass. One more timing fact had to be written into the recipe: a single SSE read buffers several frames and the SDK yields them back to back, so checking the signal only after the loop lets a cancelled run keep consuming the rest of the chunk — measured, an abort inside `handle(turn_start)` still swallowed the `done` that followed and reported success. The recipe therefore re-checks after every non-terminal frame. Finally, the behavioural matrix is no longer run against a copy of the recipe: both recipes are extracted from the document, transpiled, and **executed** with injected host objects, so the disclosure assertion really exercises the document's own `transcript.note(disclose(...))` line, and the synchronous leg gets the same full matrix the streaming one does |
254
256
  | `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". |
255
257
  | `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 |
256
258
  | `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 |
@@ -129,6 +129,22 @@ export declare const TOOL_END_INTERRUPTED_CODES: ReadonlySet<string>;
129
129
  /** 这条 `tool_end` 是不是「中断留下的合成收口帧」。未知码 ⇒ false(开集:不认得就不认得,
130
130
  * 绝不猜)。 */
131
131
  export declare function isInterruptedToolEndCode(code: string | undefined): boolean;
132
+ /**
133
+ * 提交面的 selfOrchestration 准入拒绝(`POST /v1/tasks` 与同闸的 stream 提交,**501**)。
134
+ *
135
+ * 语义:引擎侧的 workflow 开关是开着的(`SELF_ORCHESTRATION_ENABLED=true`),但这台部署处在
136
+ * **半配置的多租户形态** —— 要求 principal、却没有中心侧的准入解析器,于是它无法判「这一位
137
+ * 调用者能不能用 workflow」,fail-closed 拒。请求带 `selfOrchestration:true` **或**
138
+ * `settings.ultracode:true`(后者由 server 展开成前者)就撞这一条;把这两个键去掉,**同一条
139
+ * 请求照常受理**。
140
+ *
141
+ * 🔴 **这是复用码**:它与其它 `capability.*` 501 共用体形,而处置**不同** —— 别的能力位
142
+ * 缺席时去掉这两个键也不会变成可受理。⇒ 消费点必须按**恰等**判,绝不放宽成 `capability.`
143
+ * 前缀判(那是 `sessionMemoryStatus` 那条读面上的另一件事,两者刻意不合流)。
144
+ * 判型与「去键重发一次」的判定归 `src/selfOrchestrationDenial.ts`(文案归端)。
145
+ * 🔴 非布尔的 `selfOrchestration` 走的是 **400**,不是本码 —— 那是「值不对」不是「不给用」。
146
+ */
147
+ export declare const CAPABILITY_SELF_ORCHESTRATION_REQUIRED = "capability.self_orchestration_required";
132
148
  /** 409 session-busy 的 canonical 码([2377]C-1,server main `049ff2c`,随 5.0.0 发)。 */
133
149
  export declare const ACTIVE_RUN_BUSY_ERROR_CODE = "conflict.session_active_run";
134
150
  /** 仲裁店不可达 ⇒ **真相未知**(core 1.397 三分的第三形;塌进 `not_landed` 是诚实缺陷)。 */
@@ -182,6 +182,23 @@ export const TOOL_END_INTERRUPTED_CODES = new Set([
182
182
  export function isInterruptedToolEndCode(code) {
183
183
  return typeof code === 'string' && TOOL_END_INTERRUPTED_CODES.has(code);
184
184
  }
185
+ // ── 能力准入族(server 7.57.0 S-81;`capability.*` 是**开集前缀**,本行只登记其中一个成员)────
186
+ /**
187
+ * 提交面的 selfOrchestration 准入拒绝(`POST /v1/tasks` 与同闸的 stream 提交,**501**)。
188
+ *
189
+ * 语义:引擎侧的 workflow 开关是开着的(`SELF_ORCHESTRATION_ENABLED=true`),但这台部署处在
190
+ * **半配置的多租户形态** —— 要求 principal、却没有中心侧的准入解析器,于是它无法判「这一位
191
+ * 调用者能不能用 workflow」,fail-closed 拒。请求带 `selfOrchestration:true` **或**
192
+ * `settings.ultracode:true`(后者由 server 展开成前者)就撞这一条;把这两个键去掉,**同一条
193
+ * 请求照常受理**。
194
+ *
195
+ * 🔴 **这是复用码**:它与其它 `capability.*` 501 共用体形,而处置**不同** —— 别的能力位
196
+ * 缺席时去掉这两个键也不会变成可受理。⇒ 消费点必须按**恰等**判,绝不放宽成 `capability.`
197
+ * 前缀判(那是 `sessionMemoryStatus` 那条读面上的另一件事,两者刻意不合流)。
198
+ * 判型与「去键重发一次」的判定归 `src/selfOrchestrationDenial.ts`(文案归端)。
199
+ * 🔴 非布尔的 `selfOrchestration` 走的是 **400**,不是本码 —— 那是「值不对」不是「不给用」。
200
+ */
201
+ export const CAPABILITY_SELF_ORCHESTRATION_REQUIRED = 'capability.self_orchestration_required';
185
202
  // ── 会话冲突族 ────────────────────────────────────────────────────────────────────────────────
186
203
  /** 409 session-busy 的 canonical 码([2377]C-1,server main `049ff2c`,随 5.0.0 发)。 */
187
204
  export const ACTIVE_RUN_BUSY_ERROR_CODE = 'conflict.session_active_run';
@@ -0,0 +1,176 @@
1
+ /**
2
+ * crashConverged.ts — `GET /v1/approvals` 的 additive 键 `crashConverged` 的**三端公共读面**
3
+ * (L-38;server 7.55.0 起在场,fixture 直证坐标 `dist/boot/coordinators.js` 的 `GET /v1/approvals`
4
+ * 分支 + `dist/approval-ask-audit-store.d.ts` 的 `CrashConvergedRow`)。
5
+ *
6
+ * ## 这一格是什么(先把语义说准,文案才有得写)
7
+ * local 引擎在**人还挂在审批门上**的时候崩了:那一条 ask 既没被批也没被拒,进程一死就成了孤儿。
8
+ * server 重启后把这些孤儿**收敛成 DENIED 同码**(`decision:'denied'` + `cause:'crashed_before_park'`),
9
+ * 并把收敛结果作为一条 `crashConverged` 行挂在同一个 `/v1/approvals` 信封上。
10
+ * 对端(TUI / web / desktop)而言这是**上一条命的残留**:重连 / `--resume` 之后,人有权知道
11
+ * 「上次那次崩溃,把哪些审批替我拒掉了、其中哪几件可能已经落了一半副作用」。
12
+ *
13
+ * ## 两个桶的语义(端**唯一**需要分的那一刀)
14
+ * · `orphanState === 'pending'` = 崩的时候**工具一步都没执行**(还停在门上)。这一档配
15
+ * `resumeSafe === true` 时,重跑同一件事是安全的 ⇒ **resumeSafe 桶**。
16
+ * · `orphanState === 'decided'` = 人**当时已经批了 approve**,收敛把它翻成了 denied ——
17
+ * 可能有半截副作用落地了(文件写了一半 / 命令跑了一半)。⇒ **needsHuman 桶**,人工确认。
18
+ * · 其余一切(`resumeSafe === false`、字段自相矛盾、说不清的组合)一律进 **needsHuman**:
19
+ * 判**不**安全的代价是多问人一句(可恢复);判**错**安全的代价是让人闭眼重跑一件已经落过
20
+ * 副作用的事(不可恢复)。凡证不出来一律落保守侧。
21
+ *
22
+ * ## 🔴 caveat(DEBTS L-38 收执逐字,写文案前必读)
23
+ * `resumeSafe` 是**以账本完整为前提**算出来的**缺省值**,不是铁证 —— 崩溃现场本来就是账本最可能
24
+ * 缺页的时刻。⇒ 端的文案只许写「按记录看可以重跑」,**绝不**写「已确认没有副作用」。
25
+ * 同理:**键在场 ≠ 流内协议上场**。`crashConverged` 只是这一次 `list()` 回体上的一个 additive 键,
26
+ * 它既不宣示引擎具备什么能力,也不代表有一条推送通道会再告诉端第二次。别拿它当能力位读。
27
+ *
28
+ * ## 🔴 非目标与已知边界(对手模型成文,详见 `docs/INTEGRATION-CLIENTS.md` §12e)
29
+ * 真供给 = server JSON → SDK `JSON.parse` → 端:每一位都是**自有数据属性**,无代理、无 accessor、
30
+ * 原型恒 `Object.prototype`。**被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射**这一族**不在
31
+ * 射程内**:本件对它们只承诺三件 —— **不抛**、**不同步阻塞**、**绝不产出「可安全重跑」这个判决**
32
+ * (一律落 needsHuman 或 dropped);**不承诺**还原出「真实内容到底是什么」。一只代理在它唯一那次
33
+ * 被观察时就能给出假答案,而那与「宿主注入了一个会撒谎的传输层」是同一件事 —— 那种进程里每个对象
34
+ * 都不可信,本件不是能修好它的那一层。下面那一串防御的**唯一**目的是把这一族挡在「安全」判决之外
35
+ * 并保住线程,不是为了在敌意宿主上还原真相。
36
+ *
37
+ * ## 分工(与仓内既有形同款)
38
+ * **纯投影,零 IO、零 module 级可变态、零文案**:本件不构造 client、不认 baseUrl、不碰凭据,
39
+ * 也**一句面向用户的话都不铸** —— 措辞、是否上屏、排序与折叠全归端。
40
+ * 取件仍走既有的权威 `client.approvals.list()`(见 `hitlBridge.ts` 的 `ApprovalsResourceLike`),
41
+ * 本件只吃它的回体。
42
+ */
43
+ import type { PendingCheckpoint } from '@sema-agent/sdk';
44
+ /** 孤儿行在崩溃那一刻的状态。`'pending'` = 工具零执行;`'decided'` = 人已批,可能有半截副作用。 */
45
+ export type CrashConvergedOrphanState = 'pending' | 'decided';
46
+ /**
47
+ * `GET /v1/approvals` 回体 `crashConverged[]` 的**逐形镜像**(server `CrashConvergedRow`)。
48
+ *
49
+ * 🔴 **本包刻意不从引擎侧 re-export 这个形**(与 `editedRuleTextPrecheck.ts` 头注同一条硬约束):
50
+ * `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**,
51
+ * 包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;同门还拿 esbuild
52
+ * `--platform=browser` 真打一次包。为一个纯数据形把引擎焊进每一个装本包的端,代价与收益不成比例。
53
+ *
54
+ * 🔴 `decision` / `cause` 是**闭集字面量**,不是开集词表:这两位是本族行的**判别式** ——
55
+ * 收敛出来的行按定义就是「因为崩在 park 之前所以被判 denied」那一种。别的 `decision`/`cause`
56
+ * 组合不是「本族的新成员」,而是「这不是本族的行」⇒ 投影侧丢弃(见 {@link projectCrashConverged})。
57
+ * (对比:`engineErrorCodes` 那批 `ReadonlySet` 是**识别表**、开集纪律相反 —— 两者刻意不同族。)
58
+ */
59
+ export interface CrashConvergedRow {
60
+ /** 被收敛的那条 ask 的审批 id(端的行身份;去重/引用都按它)。 */
61
+ readonly approvalId: string;
62
+ /** 崩溃时挂在门上的工具名(展示用;UNTRUSTED-for-display,端自己消毒控制字符)。 */
63
+ readonly toolName: string;
64
+ /** 归属的 task。 */
65
+ readonly taskId: string;
66
+ /** 归属的会话(server 在场才发)。 */
67
+ readonly sessionId?: string;
68
+ /** 原 ask 铸出的时刻(epoch ms)。 */
69
+ readonly ts: number;
70
+ /** 原 ask 的到期时刻(epoch ms)。 */
71
+ readonly expiresAtMs: number;
72
+ /** 收敛结果恒为 denied(闭集判别式,见本 interface 头注)。 */
73
+ readonly decision: 'denied';
74
+ /** 收敛原因恒为「崩在 park 之前」(闭集判别式)。 */
75
+ readonly cause: 'crashed_before_park';
76
+ /** 崩溃那一刻的状态 —— 分桶的**主判据**。 */
77
+ readonly orphanState: CrashConvergedOrphanState;
78
+ /** `orphanState==='decided'` 时人当时按下的那一下(server 在场才发)。 */
79
+ readonly originalDecision?: 'approve';
80
+ /** 人按下那一下的时刻(epoch ms;server 在场才发)。 */
81
+ readonly decidedAtMs?: number;
82
+ /** 收敛动作自己发生的时刻(epoch ms)。 */
83
+ readonly convergedAtMs: number;
84
+ /** 🔴 **缺省值不是铁证**(见文件头 caveat):以账本完整为前提算出来的「重跑安全」缺省。 */
85
+ readonly resumeSafe: boolean;
86
+ }
87
+ /**
88
+ * `GET /v1/approvals` 的**信封**形。`pending` 是老形的全部;后两键 additive
89
+ * (server 按 deps 在场才发)⇒ **老形 `{ pending }` 仍然可赋值到本型**,现有消费点一个字节不用改。
90
+ *
91
+ * 🔴 `livePending` 在本包是**不透明载体**,留 `unknown[]` 有两条**互相独立**的理由:
92
+ * ① 本包对它零消费点,在这里手抄一份结构 = 手抄一份会漂的上游形(与 `ToolApprovalFrame.probeCause`
93
+ * 同一条理由);
94
+ * ② **SDK barrel 根本没把 `LivePendingRow` 导出**(实证:`@sema-agent/sdk` 7.4.0 的 `dist/index.d.ts`
95
+ * 那张 `export type { … } from "./types.js"` 名单里没有它)⇒ 想「用单一真源」也拿不到,
96
+ * 剩下的选项只有深路径 import(不是公面)或手抄(①)。
97
+ * ⇒ 想读它的端自己窄读,别指望本包替它断言形状。
98
+ * ⚠️ 方向是安全的:SDK 真实回体的 `livePending?: LivePendingRow[]` **可赋值**到 `unknown[]`,
99
+ * 所以真 `AgentClient.approvals` 仍然可赋值到 {@link ApprovalsResourceLike}(G7 段真跑 tsc 证)。
100
+ */
101
+ export interface ApprovalsListEnvelope {
102
+ pending: PendingCheckpoint[];
103
+ livePending?: unknown[];
104
+ crashConverged?: CrashConvergedRow[];
105
+ }
106
+ /**
107
+ * {@link projectCrashConverged} 的产出。**分桶 + 计数,零文案**。
108
+ *
109
+ * 🔴 `total` 只数**读得出来的**行(= `resumeSafe.length + needsHuman.length`)。供给总行数 =
110
+ * `total + dropped` —— 两个数刻意分开,免得端把「读不出来的行」算进「上次崩溃影响了 N 件」。
111
+ */
112
+ export interface CrashConvergedProjection {
113
+ /** 读得出来的行数(不含 `dropped`)。 */
114
+ readonly total: number;
115
+ /** `orphanState==='pending'` **且** `resumeSafe===true` 的行(按记录看重跑安全)。 */
116
+ readonly resumeSafe: readonly CrashConvergedRow[];
117
+ /** 其余一切(decided 臂 / `resumeSafe!==true` / 字段矛盾)—— 留人工确认。 */
118
+ readonly needsHuman: readonly CrashConvergedRow[];
119
+ /**
120
+ * 供给里**读不出来**的行数(缺必填位 / 判别式对不上 / 取属性就抛)。
121
+ * 🔴 `dropped > 0` 本身是**一条要显形的事实**:那是上游形漂了或载体坏了,端不许把它静默吃掉
122
+ * (至少留一条 debug 痕;真行被丢掉的方向,代价是人不知道有一件孤儿审批在)。
123
+ */
124
+ readonly dropped: number;
125
+ }
126
+ /**
127
+ * `GET /v1/approvals` 回体 → `crashConverged` 的**分桶投影**。纯函数,**永不抛**。
128
+ *
129
+ * ## 🔴 缺席 vs 空数组:两件不同的事,判据不许合流
130
+ * · **键缺席**(老 server / deps 不在场 / 读不动)⇒ 返回 `undefined` ——「本部署没告诉我这件事」。
131
+ * 端此时**零渲染**:绝不渲「0 个」「本次无崩溃遗留」之类的话,那是替 server 下一个它没说过的
132
+ * 断言([honest-absence-not-fabricated-zero])。
133
+ * · **键在场且是空数组** ⇒ 返回 `{ total: 0, resumeSafe: [], needsHuman: [], dropped: 0 }` ——
134
+ * 「server 明说:一条都没有」。这一档端**可以**渲「没有崩溃遗留」。
135
+ * ⇒ 两档的**返回形不同**(`undefined` vs 对象),端拿 `=== undefined` 一刀分开,不必读计数。
136
+ *
137
+ * ## 🔴 载体在场却不是数组 ⇒ 同样 `undefined`(**不**折成 `total:0`)
138
+ * 那是形漂了或中间层改写了,本包**读不出**这次的供给。折成 `{total:0}` 会让端渲出
139
+ * 「没有崩溃遗留」——在一条给人判断「能不能闭眼重跑」的面上,这是最坏方向的假断言。
140
+ * 两档合流的代价只是「都零渲染」,而分错的代价是一句用户会照着去操作的谎。
141
+ *
142
+ * ## 🔴 三处不可信读取只认**自有数据位**
143
+ * 信封的 `crashConverged`、载体的 `length`、载体的每个数字下标 —— 三处都走 `ownDataValue`:
144
+ * accessor / 缺席 / 只挂在原型上的东西一律**当缺席**,一次别人的代码都不执行(`catch` 接得住
145
+ * 「抛」,接不住「不返回」)。真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 对真行零影响。
146
+ *
147
+ * ## 🔴 载体**读不出**(已撤销 `Proxy` / `length` 或描述符取值抛)⇒ 也是 `undefined`
148
+ * 触碰载体的每一处都在保护内 —— 连 `Array.isArray()` 自己都是(对已撤销的 `Proxy` 调用它直接抛)。
149
+ * 走到一半炸掉时刻意**不交还半程结果**:一个自己都知道不全的计数,拿去渲「上次崩溃影响了 N 件」
150
+ * 比不说话更坏。
151
+ * 🔴 遍历**按数字下标**,不用载体自己的迭代协议:自带 `Symbol.iterator` 覆盖的数组能一条都不产出
152
+ * (于是真孤儿被伪造成「server 明说一条都没有」),也能把危险行替换成安全行。下标读不问载体
153
+ * 「有哪些行」这个问题。⚠️ 边界:代理仍能在 `length`/下标 trap 上撒谎 —— 那等同于「宿主注入了
154
+ * 会撒谎的传输层」,本包挡不住也不假装挡得住;本条守的是协议面。
155
+ *
156
+ * ## 🔴 进 resumeSafe 桶的合取有**五项**(两项主判据 + 两项跨位自洽 + 一项载体自证)
157
+ * `orphanState==='pending'`(工具零执行)∧ `resumeSafe===true` ∧ `originalDecision` 缺席 ∧
158
+ * `decidedAtMs` 缺席 ∧ 这一行**不带 accessor**。中间两项是**矛盾闸**:`pending` 说「一步都没执行」,
159
+ * 而那两位是「人已经按下过 approve」的证据 —— 同时在场 = 自相矛盾的载荷。最后一项是**顺序重入闸**:
160
+ * 带 getter 的行能在展开过程中把证据位删掉(见行读口头注的实测)。三者都**只挡「安全」这一侧**,
161
+ * 一律落 needsHuman 而**不丢** —— 它们可能是真孤儿,而且恰恰是最该给人看的那几条。
162
+ *
163
+ * ## 🔴 行数硬上限
164
+ * `length` 超过 100000 ⇒ 与「载体读不出」同一档(`undefined`)。判在遍历**之前**,所以一个谎报
165
+ * 十亿的 `length` trap 连一次下标读都触发不了 —— 否则同步空转会把启动/`--resume` 路的主线程冻住。
166
+ *
167
+ * ## 🔴 入参是 `unknown`,不是「带一个可选 `crashConverged` 的对象」
168
+ * 写成 `{ crashConverged?: unknown }` 会造出一个 TypeScript **弱类型**(成员全可选):把真
169
+ * `client.approvals.list()` 的回体喂进来时,SDK 7.4.0 声明的 `{ pending; livePending? }` 与它
170
+ * **一个共同属性都没有** ⇒ TS2559「has no properties in common」,文档里那句最主要的用法当场编不过
171
+ * (而只用本包自己的 `ApprovalsListEnvelope` 去测发现不了 —— 那个形恰好声明了 `crashConverged`)。
172
+ * 收 `unknown` 也更诚实:本函数对入参的全部工作**就是**窄化它,形状是运行期才知道的事。
173
+ *
174
+ * @param env 任意 `/v1/approvals` 回体(只读 `crashConverged` 一键;非对象 / `null` ⇒ `undefined`)。
175
+ */
176
+ export declare function projectCrashConverged(env: unknown): CrashConvergedProjection | undefined;