@sema-agent/client-core 0.64.1 → 0.65.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 +226 -0
- package/README.md +15 -7
- package/dist/adapt/arms.js +125 -7
- package/dist/adapt/ids.d.ts +22 -0
- package/dist/adapt/ids.js +29 -0
- package/dist/adapt/panelTasks.d.ts +22 -0
- package/dist/adapt/panelTasks.js +45 -0
- package/dist/adapt/textStream.js +6 -3
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +3 -0
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +0 -10
- package/dist/adapter/downstream/eventToSdkMessage.js +207 -65
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +4 -4
- package/dist/adapter/downstream/terminalToSdkResult.js +43 -12
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +23 -2
- package/dist/adapter/downstream/turnUsageToModelUsage.js +7 -1
- package/dist/adapter/runStream.js +39 -2
- package/dist/adapter/types.d.ts +12 -0
- package/dist/autoModeUnavailable.d.ts +48 -79
- package/dist/autoModeUnavailable.js +70 -99
- package/dist/classifierStatus.d.ts +25 -71
- package/dist/classifierStatus.js +110 -105
- package/dist/cloudConfigWireCaps.js +16 -0
- package/dist/decideReceipt.d.ts +117 -0
- package/dist/decideReceipt.js +142 -0
- package/dist/engineErrorCodes.d.ts +32 -0
- package/dist/engineErrorCodes.js +42 -0
- package/dist/fleet/fleetProjection.d.ts +24 -1
- package/dist/fleet/fleetProjection.js +26 -1
- package/dist/fleetAgentPanelProjection.js +6 -1
- package/dist/gateVocabulary.d.ts +9 -1
- package/dist/gateVocabulary.js +46 -4
- package/dist/hitl/askGateWire.js +22 -1
- package/dist/hitl/gateLedger.d.ts +24 -0
- package/dist/hitl/gateLedger.js +8 -0
- package/dist/hitl/hitlBridge.js +14 -2
- package/dist/hitl/parkResolver.d.ts +23 -2
- package/dist/hitl/parkResolver.js +34 -6
- package/dist/hitl/toolApprovalWire.d.ts +94 -6
- package/dist/hitl/toolApprovalWire.js +127 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -6
- package/dist/mcpWireCaps.d.ts +31 -0
- package/dist/mcpWireCaps.js +12 -0
- package/dist/notifications.js +11 -2
- package/dist/runTerminal.d.ts +48 -0
- package/dist/runTerminal.js +59 -0
- package/dist/seam.d.ts +131 -1
- package/dist/seam.js +22 -0
- package/docs/INTEGRATION-CLIENTS.md +934 -61
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,232 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.65.0(2026-09-11)
|
|
53
|
+
|
|
54
|
+
> 两半场同版:**① 投影臂族归层修**(B-071 / B-072 / B-073 / B-074 / L-215③)与
|
|
55
|
+
> **② 接入审计 B 战役的包侧归层四件**(B-070 / B-078 / L-215①② / B-080)。接入面详解见
|
|
56
|
+
> `docs/INTEGRATION-CLIENTS.md` §30(①–⑥ = §30a–§30g,⑦–⑩ = §30h–§30n)。
|
|
57
|
+
> 🔴 **本段有 BREAKING**(熔断族公面删除,见 B-080 那节)——其余全为 additive。
|
|
58
|
+
|
|
59
|
+
### 投影臂族归层修(B-071 / B-072 / B-073 / B-074 / L-215③)
|
|
60
|
+
|
|
61
|
+
本批五件共一条形:**上游早已把事实摆在 wire 上,而包边界这一格空转或把它折掉了**。四件是「键被剥/
|
|
62
|
+
被折/被铸成假值」,一件是「读器拿装配期事实回答了每次调用的问题」。**本半场**全部 additive、零破坏性
|
|
63
|
+
变更(BREAKING 只出在下半场的 B-080;详见 `docs/INTEGRATION-CLIENTS.md` §30a–§30g)。
|
|
64
|
+
|
|
65
|
+
- **B-072 压缩边界帧三件**(`adapter/downstream/eventToSdkMessage.ts`)
|
|
66
|
+
- `compact_metadata.trigger` 由 `manual ? manual : auto` 的**三词折叠**改为**开集透传**:core 的臂注
|
|
67
|
+
逐字说 wire 现在带的是钩子层看见的那个值(`the SAME value the hooks saw instead of flattening it
|
|
68
|
+
to "auto"`),包在下游又折一次正是上游刚停止做的事。`forced`(压缩不是可选的那两条腿)从此到得了
|
|
69
|
+
宿主;缺席 / 空串 / 非串仍回落 `auto`。**CC 消费者按 `!== 'manual'` 分支的行为逐字不变。**
|
|
70
|
+
- 新增超集键 `compact_metadata._sema_preserved_segment`(⇐ core `preserved_segment`,压缩分割线的
|
|
71
|
+
保留段锚)与 `_sema_clamped_ratio`(⇐ core `clampedRatio`,摘要输入被钳掉的比例,真 0 是披露事实)。
|
|
72
|
+
两者**缺席一律不铸**;`preserved_segment` 载体在而 id 读不出 ⇒ 整键不出现(半个锚比没有锚更坏)。
|
|
73
|
+
- `message_committed` 由 `not_in_slice` 改投**中性内部臂**,并新增**可选** chrome 臂
|
|
74
|
+
`message_committed`(`entryId` / `role` / `toolCallId?` / `eventId?`)。它是上一条那个锚的**唯一**
|
|
75
|
+
解析腿(core 亲口:消费方用本帧自建 `entryId → message` 映射)。仍**绝不铸 transcript 行**。
|
|
76
|
+
- **B-073 成本可分性两件**(`terminalToSdkResult.ts` / `turnUsageToModelUsage.ts`)
|
|
77
|
+
- `total_cost_usd` 由 `usd > 0` 改为照实报:**显式 0 = 声明免费**(core 逐字 `an explicit all-zero
|
|
78
|
+
Model.cost still reports 0`),缺席 / 非有限 = 没定价 ⇒ `null`。负数原样过境。
|
|
79
|
+
- `ModelUsage.costUSD` 保持 CC 形不破(必填 number),缺席由**同行**新增的超集判别位
|
|
80
|
+
`_sema_cost_absent: true` 说 ⇒ **两键合读**:`costUSD===0` + 位缺席 = 免费;+ 位 `true` = 没定价。
|
|
81
|
+
判别位**从不铸 `false`**。新导出类型 `SemaModelUsage`(= `ModelUsage` 的超集,旧消费者零改)。
|
|
82
|
+
- `usage.cacheCreationInputTokens` 不再硬编 `0`,读 `TaskStats.cacheWriteTokens`(合成 modelUsage 行
|
|
83
|
+
同形第二处一并扫);扁平 usage 四格由 `?? 0` 改**有限数守卫**(开集索引 + JSON wire 会让 `"12"` /
|
|
84
|
+
`NaN` / `Infinity` 原样落进型面写着 `number` 的槽)。
|
|
85
|
+
- **B-074 子代终态那一拍**(`adapter/downstream/eventToSdkMessage.ts` / `adapt/arms.ts` /
|
|
86
|
+
`adapt/panelTasks.ts`)
|
|
87
|
+
- 投影层补 `task_progress.status`(**开集**透传,缺席 / 空串 / 非串不铸)。
|
|
88
|
+
- adapt 层:终态 tick(`completed` / `failed`)**落行** —— 新增 `PanelTaskLedger.settleFromTerminalTick`
|
|
89
|
+
(按 taskId、走 `laneOf` 的车道证明、冻结 inline 孪生、清常驻账、`SubagentStop` 走 if-started 门、
|
|
90
|
+
与另两条 settle 腿共用 `endedPanelTasks` 判重)+ 新增只读口 `isLiveBound`。
|
|
91
|
+
🔴 **两条承重细节**(异源对抗复审逼出):① 这一拍**先把最终累计数字落到行上**再落终态
|
|
92
|
+
(早退会让行冻在倒数第二拍);② **只落「关卡腿不会来收」的行**(`!isLiveBound`)——
|
|
93
|
+
只有关卡那条腿带得动子代的**最终报告**,抢先落行会让它撞判重整条让位。
|
|
94
|
+
- ⚠️ **已知边界**(如实移交):代际 `seq` 不参与判重 ⇒ 同一 taskId 复活后第二代的终态落不出来。
|
|
95
|
+
不在本批修(三条 settle 腿里关卡腿根本没有 `seq`,半个代际模型会导致**重复**落终态);
|
|
96
|
+
今天的行为已由 `run-task-progress-terminal-projection-test.mjs` **B9** 段钉住。
|
|
97
|
+
- **三条不改判**:`running`、**缺席**(旧引擎不发这一格)、以及开集里的第四个词 —— 都走修前那条路。
|
|
98
|
+
- **L-215③ assistant 行身份键**(`adapter/types.ts` / `eventToSdkMessage.ts` / `adapter/runStream.ts`)
|
|
99
|
+
- `message.id` ⇐ wire `eventId`(**确定性**:live 与 durable replay 同值;缺席不铸,绝不现铸随机 id)。
|
|
100
|
+
- `message.model` ⇐ 新增的 `EmitContext.model`(宿主开流时钉;缺席不铸,**绝不猜**)。
|
|
101
|
+
- 🔴 `usage` / `stop_reason` **刻意不铸在 assistant 臂上**(帧序:内容臂先到、`turn_end` 后到,臂发出
|
|
102
|
+
时引擎还没报账 ⇒ 在那里铸只能是估算)。真值出口 = `turn_usage` 臂的两个超集键
|
|
103
|
+
`_sema_last_assistant_usage`(与 footer 折叠共用同一只 `turnEndUsage()` 产物)与 `_sema_stop_reason`
|
|
104
|
+
(`turn_end.stopReason` 原词开集透传);chrome 腿的对偶 = `last_turn_usage` 新增的
|
|
105
|
+
`stopReason` / `usageMissing` 两位。
|
|
106
|
+
- 🔴 **两条诚实缺席**(异源对抗复审逼出):`turn_end.usageMissing === true` ⇒ **不铸**镜像、改铸
|
|
107
|
+
`_sema_usage_missing`(全零的「不知道」绝不冒充一笔已知的零账);`stopReason` **不再被 usage 的
|
|
108
|
+
存在性吞掉** —— 发臂条件由「有 `outputTokens`」放宽到「有话可说」。**有意的行为变更**:此前发
|
|
109
|
+
0 条的那一格(`{usageMissing:true, stopReason:…}`)现在发 1 条,且不带 `outputTokens` 键 ⇒ 以
|
|
110
|
+
`typeof === 'number'` 开门的既有消费者对它 no-op。
|
|
111
|
+
- 🔴 **身份两位穿过第二层投影**:`adapt` 的五个 assistant 构造点都是重建 `{role,content}` 的,现由
|
|
112
|
+
`adapt/ids.ts` 的 `messageIdentityOf` 单点接力(`model` 帧优先 ctx 兜底、`id` 只认帧)。
|
|
113
|
+
两处**刻意例外**:tool_use 卡臂与终帧补差腿只接 `model`(那两条的 `id` 是另一种身份)。
|
|
114
|
+
`AdapterContext` 同批新增 `model?: string` 座位(live 分段腿的兜底)。
|
|
115
|
+
- **B-071 分类器状态面拆词**(`classifierStatus.ts`)
|
|
116
|
+
- `CLASSIFIER_STATUS_STATES` 新增 `armed`(插在 `available` 之前);与本批 B-080 的熔断族退役合成后
|
|
117
|
+
是**三词** `armed` / `available` / `unavailable_this_round`(`breaker_open` 同批删)。
|
|
118
|
+
`autoMode.armed === true` 现在判 **`armed`** 而不是 `available` —— core 把「decider 为这条腿铸出来了」
|
|
119
|
+
(装配期)与「可用性是 per-CALL 事实」分得很清楚,读器此前拿前者答了后者。
|
|
120
|
+
- `available` 只从**本轮肯定事实**得出:一只 `origin === 'denial_limit_fallback'` 的 ask / 门记录
|
|
121
|
+
(core:`the classifier stands BEFORE this ask`)。别的 origin 一个都不算,理由逐条写在源码里。
|
|
122
|
+
- `classifierStatusDetail` 新增 `armed` **一句**(熔断那半句随 B-080 的退役一并不存在),与既有各句
|
|
123
|
+
**逐字互异**且不说「在跑」。
|
|
124
|
+
- 端影响:`/doctor` 那行的词由本包单铸 ⇒ **端零改即接通新词**;但若端自己 `switch` 过状态词,
|
|
125
|
+
需要加 `armed` 一臂(措辞面无此义务)。
|
|
126
|
+
|
|
127
|
+
### B-070 / L-200 `/decide` 的 200 是**投递受理**,不是「门已解决」
|
|
128
|
+
|
|
129
|
+
- 🔴 `hitlBridge.decideRaw` 的 decide 成功臂**不再清 `active`** —— sdk README §9.0.0 逐字:「workflow
|
|
130
|
+
车道的 200 只是**投递受理**……据 200 立刻把卡从 UI 抹掉,用户会看到一张『批过了却还在』的幽灵卡」。
|
|
131
|
+
判据锚换到流上的**下一条 running 臂**(`observe()` 那条腿修前就在,本批只是拿掉抢跑的那一行)。
|
|
132
|
+
- 🔴 **换挂**:`ParkResolution` 新增 `handoffTaskId`(判据 = 回执 `status:"resuming"` ∧ 非空 `taskId`,
|
|
133
|
+
且与当前 taskId 不同),`askGateWire` 据它换挂 `runs.events` 并**同时丢掉 `lastEventId`**
|
|
134
|
+
(durable seq 是 per-run 的);`GateLedger` 新增 `forgetSeq()`。
|
|
135
|
+
- **新模块 `src/decideReceipt.ts`**(additive 公面 7 件):`readDecideReceipt` 读 200 的四种形;
|
|
136
|
+
`executionOutcome` 走 `gateOutcome.ts` 的**同一只**读器,🔴 **缺席 = 未知,禁读成 allowed**;
|
|
137
|
+
`decideRefusalFromError` 给三条 workflow 车道拒绝码各一句人话 + 按出处取值的 `resendable`。
|
|
138
|
+
码常量(4 件)在 `engineErrorCodes.ts`。
|
|
139
|
+
|
|
140
|
+
### B-078 / L-208 `approval_request` / `approval_revoke` 两条 chrome 臂(归层修)
|
|
141
|
+
|
|
142
|
+
- 两臂不再 `dropped('unsupported_arm')`,改投同名 chrome 事件,**载荷 = 帧原样窄读**(信封纪律:
|
|
143
|
+
不窄化到 v1、不摘键);坏信封 ⇒ `dropped('malformed')` 而不是静默 `none`。
|
|
144
|
+
- `CHROME_ARM_TABLE` 新增两行 **`required: true`**,消费义务写在 `seam.ts` 的两只接口头注里。
|
|
145
|
+
- 🔴 **对宿主是新义务**:不接 = 流内审批卡整面哑掉(重连后 pending 卡恢复不了 / 撤销后卡不清)。
|
|
146
|
+
|
|
147
|
+
### L-215①② fleet `parked` 与「非成功终局」的单铸谓词
|
|
148
|
+
|
|
149
|
+
- `coerceWorkflowStatus` 认 `parked` ⇒ `awaiting approval`(与 `coerceTaskStatus` 同落点)。
|
|
150
|
+
- `runTerminal.ts` 新增 `TERMINAL_NOT_SUCCESS_STATUSES` / `isTerminalNotSuccess` /
|
|
151
|
+
`TERMINAL_STATUSES` / `isTerminalStatus`(additive 公面 4 件)。🔴 `blocked` 在表里(core [6908]:
|
|
152
|
+
agent 自报终态),`suspended`/`needs_review` **不在**(那是等人)。
|
|
153
|
+
- **同形存量族扫**:`notifications.ts` 两处、`fleetAgentPanelProjection.ts` 一处、`adapt/arms.ts` 一处
|
|
154
|
+
的内联三词判据一次改齐。
|
|
155
|
+
|
|
156
|
+
### B-080 / L-210 闭集镜像过期族
|
|
157
|
+
|
|
158
|
+
- **devDep `@sema-agent/core` `~7.10.0` → `~7.12.0`**(镜像门参照物随引擎)。
|
|
159
|
+
- 🔴 **BREAKING(公面删两件)**:`AUTO_MODE_BREAKER_CAUSES` / `classifierBreakerOf` 随 core 7.12.0 的
|
|
160
|
+
Removed(BREAKING) 整只退役(clean-cut,不留别名);同批删 **型** `ClassifierBreakerView`、
|
|
161
|
+
`ClassifierStatusView.breaker` 一位、`WiringManifestAutoMode.breaker` 一位,
|
|
162
|
+
`CLASSIFIER_STATUS_STATES` 删 `breaker_open` 一态,`AUTO_MODE_UNAVAILABLE_CAUSES` 收成 `['error','timeout']`。
|
|
163
|
+
⚠️ 读过它们的宿主编译红 —— 删读点即可(状态面自然回落;与本批 ⑥ B-071 新增的 `armed` 合成后是**三态**
|
|
164
|
+
`armed` / `available` / `unavailable_this_round`)。
|
|
165
|
+
- **AskOrigin 三句订正**(行为面,措辞):`unresolvable`(祖先标记 + 必达 park)/
|
|
166
|
+
`shell_gate_tighten`(这一档以上,不是每一条)/ `rule_store_unavailable`(中性定义句 +
|
|
167
|
+
`askOriginDetail(origin, message?)` 的 additive 第二参让位给引擎的 `message`)。
|
|
168
|
+
|
|
169
|
+
### 常驻门
|
|
170
|
+
|
|
171
|
+
🆕 = **新增八道**(四道来自 ①–⑥,四道来自 ⑦–⑩);其余行是同批**改口**的既有门。
|
|
172
|
+
|
|
173
|
+
| 门 | 守什么 |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `scripts/run-compaction-boundary-projection-test.mjs` | 🆕 B-072 三键 + `message_committed` 两面(中性臂 / chrome 臂);`clampedRatio` 的 sdk 座位带**退出条件**(补上当天红逼复核 raw 读) |
|
|
176
|
+
| `scripts/run-cost-absence-projection-test.mjs` | 🆕 B-073:`total_cost_usd` 显式 0 ≠ 缺席、`costUSD` + `_sema_cost_absent` 两键合读、`cacheCreationInputTokens` 读真值、扁平四格有限数守卫 |
|
|
177
|
+
| `scripts/run-task-progress-terminal-projection-test.mjs` | 🆕 B-074:`status` 开集透传 + 终态落行 + 三条不改判 + 幂等 + workflow lane 门在先 |
|
|
178
|
+
| `scripts/run-assistant-arm-identity-test.mjs` | 🆕 L-215③:`id`/`model` 两键与它们的缺席语义、`usage`/`stop_reason` **不在臂上**的反向钉、`turn_usage` 两超集键 |
|
|
179
|
+
| `scripts/run-classifier-status-test.mjs` | 🆕 **G 段**(B-071):三词表、`armed` ≠ `available`、`denial_limit_fallback` 是唯一正向证据(其余十一词逐词负控)、优先序、措辞三句互异;`per-CALL` 那句的 core 7.12.0 **逐字锚** —— B-080 把 devDep 抬到 `~7.12.0`,C1 车那条「领先登记」按它自己写的退出条件当场销账换成正向断言。🔁 同批:熔断族两侧退役反钉(A 段) |
|
|
180
|
+
| `scripts/run-integration-doc-freshness-test.mjs` | §3c 臂表(`message_committed` 离表)、`CLASSIFIER_STATUS_STATES` 成员换代(表长仍 3)、`AUTO_MODE_UNAVAILABLE_CAUSES` 表长入档、本节新增坐标 |
|
|
181
|
+
| `scripts/run-decide-receipt-test.mjs` | 🆕 B-070:`/decide` 200 = 投递受理的**源码反钉**(成功臂零 `this.active = null`、`observe()` 清除腿还在)、四种 200 形、`handoffTaskId` 两件合取、`executionOutcome` 缺席 ⇒ undefined、三码三句 + 按出处取 `resendable`、表外 `decide.*` 码判 `null` |
|
|
182
|
+
| `scripts/run-approval-frame-chrome-arms-test.mjs` | 🆕 B-078:两条 chrome 臂的**信封原样**过境(不窄化到 v1、未知 schemaVersion 照过)、坏信封 ⇒ `dropped('malformed')` 非静默、`CHROME_ARM_TABLE` 两行 `required:true` 的义务文本、**刻意无子流断闸** |
|
|
183
|
+
| `scripts/run-terminal-status-vocabulary-test.mjs` | 🆕 L-215①②:`isTerminalNotSuccess` / `isTerminalStatus` 单铸谓词(`blocked` 在表、`suspended`/`needs_review` 不在)、`parked ⇒ awaiting approval` 端到端、**同形存量族扫**反钉(`src/` 零内联三词判词) |
|
|
184
|
+
| `scripts/run-retired-vocabulary-census-test.mjs` | 🆕 B-080②:退役词汇普查 —— 每条登记项「下游代码位置零命中 + 公面零导出」+ 🔴「上游存活断言」两件;今天登记五条(熔断族四符号 + `breaker_open` 词);反向钉:退役**说明**必须留在注释里 |
|
|
185
|
+
|
|
186
|
+
另有四道既有门同批改口:`run-auto-mode-unavailable-test.mjs`(熔断族两侧退役反钉 + 成因表两词)/
|
|
187
|
+
`run-gate-vocabulary-test.mjs`(含 D2 uplift 锚的归属裁定:候 sdk 镜像)/ `run-client-core-pure-test.mjs` /
|
|
188
|
+
`run-hitl-gate-honesty-test.mjs`(`armed ⇒ available` 反向控随 B-071 订正)。
|
|
189
|
+
|
|
190
|
+
## 0.64.2(2026-09-10)
|
|
191
|
+
|
|
192
|
+
> server 7.69.0 提货批的包侧六件:**四件 additive**(零删除;只在上游真发那一位在场时多铸一个键)
|
|
193
|
+
> + **一条行为面订正**(件④,异源对抗复审 r1 [medium] 逼出;`parse_error` 的行为一字未变)
|
|
194
|
+
> + **一处同形存量族扫**(件⑤)。接入面详解见 `docs/INTEGRATION-CLIENTS.md` §29。
|
|
195
|
+
|
|
196
|
+
### 新增
|
|
197
|
+
|
|
198
|
+
- **`ApprovalCardRequest.classifierUnavailable?: { cause: string }`**(cli L-174②;server ≥7.69.0 /
|
|
199
|
+
core 7.10.0 #616)——「问你是因为 auto 模式分类器这一轮跑不了」的事实**两腿都到卡口**:活卡帧腿
|
|
200
|
+
(`ToolApprovalFrame.classifierUnavailable`,同批进帧型与键镜像)与 durable park 行腿(行上
|
|
201
|
+
`PendingAction.tool_approval` 的孪生位),经**同一把**窄读器,**键路与 ask / park 行同形** ⇒ 端把卡入参
|
|
202
|
+
直接喂给 0.63.0 就有的 `classifierUnavailableOf` / `classifierUnavailableDetail`,读法零改。
|
|
203
|
+
过境判据与 server 唯一铸点 `readClassifierUnavailable` 同源(`cause` 非空串即收、**开集**、坏形降缺席);
|
|
204
|
+
「渲哪一句」那一层的闭集收窄仍归公面读器 —— **过境按开集、显示按闭集**是设计,不是漂移。
|
|
205
|
+
🔴 显示元数据、echo-only:不参与也不改变任何决断字节,缺席**不许**渲成「分类器跑成了」。
|
|
206
|
+
⚠️ 两条诚实边界:① 本键**退役日期已定**(core 7.12.0 / server 7.70.0 起帧上是死键,事实位随拒绝面走),
|
|
207
|
+
0.65.0 改读那一面而**不删码**;② durable 腿在 engine 7.69.0 上**恒零命中** —— 两只 checkpoint 店的
|
|
208
|
+
`listPending` 今天没把这一位扁平化到 `/v1/approvals` 行上(与 `governanceForced` 同一格),包侧承诺是
|
|
209
|
+
「行上有 ⇒ 卡上有」,server 补投那天零改动生效。
|
|
210
|
+
- **durable park 行 → 卡的两位对偶**(件⑥;cli B-058)——「行 → 卡」重铸此前只投 5 位,补上
|
|
211
|
+
`persistedRuleShadowed`(← 行 **`riskDescriptor.shadowedRule`**;⚠️ **键名与活卡帧顶层不同**,那正是它
|
|
212
|
+
长期被漏掉的原因 —— server `redactPendingDisclosures` 逐字脱敏后随 `projectPendingForWire` 上 wire)与
|
|
213
|
+
`ruleOffersAbsence`(← 行同名键;两只 checkpoint 店今天还没把它扁平化上来,补投那天零改动生效)。
|
|
214
|
+
落位是**活卡腿同一个卡位**,端零改。🔴 **它是壳让位臂的输入,不是一格顺手补上的显示**:活卡腿早就有,
|
|
215
|
+
durable 腿因为不投,一条**被记住的 allow 规则清不掉的 ask** 在耐久路上看起来就是普通 ask ⇒ 被自动
|
|
216
|
+
`decide approve`(cli B-058 离线真跑:default 车道 + `Bash(grep:*)` + 带 `shadowedRule` 的 park 行 ⇒
|
|
217
|
+
零卡 + approve)。判据与活卡腿逐字同族(非空白串才 stamp、原文字节、坏形/缺席不折空串、两位不连坐)。
|
|
218
|
+
同批把帧上 `persistedRuleShadowed` JSDoc 里「耐久对偶仍未消费——那一路是独立的一件」那句 0.28.0 旧话
|
|
219
|
+
改成现状。黑盒判据 **G-41**。
|
|
220
|
+
- **`readDenialLimitFallback` 上公面**(cli L-174③)—— 0.59.0 起就在的限额回落卡窄读器
|
|
221
|
+
(四成员全必填、计数与窗按有限非负数、`limit` 开集)从模块私有转为导出,让端自持的同判据副本整只退役
|
|
222
|
+
(第二个判官会在上游改形那天与包各漂各的)。⚠️ 导出的是**读器,不是许可**:`autoDenyAfterMs` 仍然只许
|
|
223
|
+
渲倒计时,窗的执行全在引擎。公面导出 918 → **919**。
|
|
224
|
+
|
|
225
|
+
### 修复
|
|
226
|
+
|
|
227
|
+
- 🔴 **`classifierUnavailableOf` 把「比本端新的成因词」吞成缺席**(件④;**本批唯一的行为面改动**,
|
|
228
|
+
异源对抗复审 r1 [medium] 采纳)—— 修前这只 0.63.0 的读器按 `AUTO_MODE_UNAVAILABLE_CAUSES` **闭集**
|
|
229
|
+
收窄,于是一台比本端新的引擎发一个**合法的新成因词**时,审批卡上「为什么问我」那一行**整段消失**;
|
|
230
|
+
把同一张卡喂给 `classifierStatusOf({armed:true}, card)` 还会答 **`available`** —— 一条真实的故障事实
|
|
231
|
+
被渲成「一切正常」。自证:`classifierUnavailableDetail` 早就为这一形备了兜底句
|
|
232
|
+
(「a word newer than this client」),而修前那句话**永远不可达**。
|
|
233
|
+
修后判据 = **非空串即收 + 只排除熔断轴独占的词**(`AUTO_MODE_BREAKER_CAUSES` −
|
|
234
|
+
`AUTO_MODE_UNAVAILABLE_CAUSES`,**派生而非手抄**,今天 = `{parse_error}`)——「表外」与「另一条轴上
|
|
235
|
+
的词」本来就是两件事,修前把它们判成了同一件。🔴 **`parse_error` 的行为一字未变**,0.63.0 起的黑盒
|
|
236
|
+
判据 **G-19 照旧成立**;新判据的判据见新黑盒判据 **G-36**。端**零改**,但端若自己维护过一张成因闭集
|
|
237
|
+
表(为了「只渲认得的词」),那张表现在是这次修的病的第二处,该退役。
|
|
238
|
+
- **`cloudMcpToSpecs` 同样丢 `toolFaces`**(件⑤,下一条的**同形存量第二处**;族扫抓出)——
|
|
239
|
+
center `/api/v1/effective` 的 mcp 域 → 请求面的那条投影也是逐键白名单重建。上游对这一位的成文是
|
|
240
|
+
「三条腿(请求 / config-center / 本地 config.d)同一条规则」,只修本地那条 = 用 center 下发配置的
|
|
241
|
+
部署照样丢键。判据与本地腿逐字相同;门 G7 段改成**对两条腿各对一次账**。
|
|
242
|
+
- **`mcpConfigToSpec` 丢 `.mcp.json` 的解析透明键 `toolFaces`**(cli L-167①,包侧缺陷)——
|
|
243
|
+
该函数是逐键白名单重建,而 settings-schema 1.9.0 给 mcp 段加的 `toolFaces` 是**解析透明键**
|
|
244
|
+
(`z.unknown()`,形的校验属引擎摄入侧),白名单没有这一行 ⇒ 运维在 `.mcp.json` 上声明的 per-tool 面
|
|
245
|
+
(写围栏与敏感路径策略读的 `pathTarget` 那一格)在**包边界**被静默剥掉,而两边看着都对。
|
|
246
|
+
修 = **按上游的透明键表逐名透传**(两条 transport 臂 + 整表腿),**不是**改成全展开:配置上的未知键照旧
|
|
247
|
+
不上 wire。🔴 **坏形原样过境,不是包拒** —— 形的判决在引擎(整条 server 不进 + `mcp_injection_dropped`
|
|
248
|
+
点名 / 单只工具拒 + `config.tool_face_invalid`);包在这里判一次形,判严了就把运维声明的保护**静默**吞掉。
|
|
249
|
+
透明键表 1.10.0 实装两员,`source` 刻意不在这条腿上(它是部署面的键,请求面 `McpServerSpec` 上没有它),
|
|
250
|
+
这条不透传是**带退出条件的记账**。
|
|
251
|
+
|
|
252
|
+
### 文档
|
|
253
|
+
|
|
254
|
+
- `docs/INTEGRATION-CLIENTS.md` 新增 **§29**(本批六件:病形、两腿投影表、五条读法、两条诚实边界、
|
|
255
|
+
三端换装清单、黑盒判据 G-33–G-39、常驻门);§0a 版本锚整段刷到 0.64.2;§2/§2b 计数 918 → 919。
|
|
256
|
+
- §23a 的 `denialLimitFallback` 读器纪律段指向已上公面的 `readDenialLimitFallback`,并写明端自持副本
|
|
257
|
+
应当退役的理由与「导出的是读器不是许可」。
|
|
258
|
+
- §27h 单铸律补第二条 `delegation.ask_unresolvable`(core 7.11.2 `ask-unresolvable-notice.d.ts` 的
|
|
259
|
+
`@contract` 逐字引用):audience `user`、`detail` 五格、🔴 `parkLaneExisted` 是判别位禁折叠、
|
|
260
|
+
deny 本身比通告更老且一字不改(披露面补齐,不是裁决面变更)。
|
|
261
|
+
- §27i⑦ audience 分流条款补一条**允许形**:单人本机端(起引擎的人 = 看屏的人)可以**登记在案地**不分流、
|
|
262
|
+
把 `operator` 受众渲通用行 —— 这一端没有第二块屏,分流的净效果是把一批正文里就写着补救办法的治理事实
|
|
263
|
+
整段静音。允许的是渲通用行,**不是不登记**(确实不渲的码要进端自己的明拒表);多用户 / 有独立运维面的端
|
|
264
|
+
照旧分流。§27i⑧ 改成与本批实装一致(改读卡入参;过境/显示两层分工;退役日期)。
|
|
265
|
+
|
|
266
|
+
### 常驻门
|
|
267
|
+
|
|
268
|
+
- `run-hitl-gate-honesty-test.mjs` 新增 **F16 段**(活卡帧腿,含件④ 的**端到端**格:新词一路到最终
|
|
269
|
+
那句话与状态读数,带反向判别力自证),`run-durable-card-display-keys-test.mjs` 新增 **⑫ 段**
|
|
270
|
+
(durable 腿,含**两腿同值反向对照**、端到端与射程边界打印),`run-approval-frame-keys-test.mjs`
|
|
271
|
+
新增 `classifierUnavailable` 的 `AHEAD_OF_ANCHOR` 领先登记(sdk 8.8.0 锚 27 项尚无本键;两条退出条件由门
|
|
272
|
+
自己执行),`run-additive-key-passthrough-test.mjs` 新增 **G7 段**(透明键表 ⇄ **每一条腿**的账逐名相等,
|
|
273
|
+
本地 + center 两条腿各对一次),`run-auto-mode-unavailable-test.mjs` 新增 **B2b 段**(件④ 的红先绿后:
|
|
274
|
+
未知词照收并走到兜底句 / 排除集是派生的 / 两集的交照旧读得出)。
|
|
275
|
+
- 棘轮:公面导出基线 918 → **919**;`unknown` 出境 312 → **314**(逐件写明理由);
|
|
276
|
+
`TOOL_APPROVAL_FRAME_KEYS_MIRROR` 计数 27 → **28**。
|
|
277
|
+
|
|
52
278
|
## 0.64.1(2026-09-09)
|
|
53
279
|
|
|
54
280
|
### 文档
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.65.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
|
|
@@ -230,14 +230,14 @@ public-surface guard checks that last one).
|
|
|
230
230
|
| `scripts/run-client-core-portability-test.mjs` | Kernel / A-layer / index import closures, the runtime-dependency equality gate, barrel reachability, and a real esbuild `--platform=browser` bundle |
|
|
231
231
|
| `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
|
|
232
232
|
| `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise |
|
|
233
|
-
| `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal) |
|
|
233
|
+
| `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal — the register is occupied again by the classifier-unavailable key the server already emits, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption) |
|
|
234
234
|
| `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
|
|
235
235
|
| `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
|
|
236
236
|
| `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
|
|
237
237
|
| `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code |
|
|
238
238
|
| `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but carries no such key (an older engine), the engine explicitly answered `null` — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock |
|
|
239
239
|
| `scripts/run-terminal-cause-projection-test.mjs` | The `7.64.0` wire reshape, projected. A run's ending stopped being eight parallel flat keys and became **one tagged cause** (`completed | failed | blocked | paused`), and a tool call's gate stopped being four orthogonal words and became **one record** (`disposition` / `settlement?` / `origin?`). Both are read in exactly one place in this package, and this guard pins them at **two levels**, because the dangerous seam is "the reader was updated, the consumer was not": each terminal arm is checked on the reader *and* on the `subtype` / `is_error` / `errors[]` the projector actually emits. Two properties carry most of the weight. First, a terminal word this reader does not know is **never** laundered into an empty success — it lands on an `unknown` arm carrying the word verbatim, while a payload with no terminal word at all (the mock lane) keeps the success arm exactly as before, which is the one and only case the reader answers `null`. Second, the three window words (`approval_window_expired`, `denial_limit_window_expired`, `park_sla_expired`) must each be told apart by a different predicate: the previous generation collapsed all three onto one `timeout`, and re-merging them would throw away the discrimination this reshape just restored. Two byte generations are read by one reader, keyed on the discriminator upstream nailed (`"terminal" in result`): the current cause form, and the **flat** form that a current engine still emits on two lanes — replayed persisted bytes, which the service passes through verbatim rather than back-filling, and the service's own rejection envelope. A cause-form payload that also carries stale flat keys must ignore them entirely: keeping one compatibility read is what gives a single fact two sources. The same file also pins the MCP delivery verdict and HTTP status riding the wiring manifest, the four-state write-protection reading (where three of the four states mean *cannot tell*, and none of them may be printed as "there is no table"), and the park-reopen fetch identity: that predicate is asserted through the **real entry point**, since the defect being fixed was precisely a call site wired to a different predicate than the one that routed the row there |
|
|
240
|
-
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted.
|
|
240
|
+
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. The cause table is a **copy**, reconciled word for word in both directions against the installed engine's own bytes — it narrowed upstream, and the guard follows rather than keeping the old shape: a table checked against something nobody ships any more is the oldest way for a guard to be green and wrong. The retirement is held from both sides — the removed table must really be gone upstream, and the removed reader and word must really be gone here — while the word that left keeps arriving cleanly from an older engine, because the reader takes the cause as an **open set**: the vocabulary belongs upstream, so a copied list here would discard a legal value the day one is added, and the value discarded is precisely "this outage is a NEW kind". The reader's one exclusion is the word the engine says it never stamps here — the classifier did run and did answer, just outside its contract, so reading it as a failure would invent an event the engine denies. That exclusion used to be derived from a second table which no longer exists; the reason for it never lived in that table, so it is now stated where it actually comes from, pinned as a **named** set (a magic literal scattered through the reader reds) and cross-checked against the engine's own verdict declaration and against the reader having exactly one such comparison. One reader serves both the live ask and its durable parked twin, since the two carry the same key path and a second copy is how two ledgers drift apart. Absence is pinned as absence — most asks never consulted a classifier at all — and the sentences are checked mutually distinct, prototype-safe, and walked end to end: an unknown word reaches the sentence a person reads (the fallback that names it verbatim) and the status reading (unavailable for this round, never a fallback to "available"), with counter-controls proving neither assertion is vacuous |
|
|
241
241
|
| `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails |
|
|
242
242
|
| `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever |
|
|
243
243
|
| `scripts/run-permission-rule-issue-codes-test.mjs` | The rule-lint refusal codes an engine reports when it will not compile a permission rule. The SDK publishes neither a schema nor a type for them, so the package mints the table from the engine's own bytes and the guard pays the cost of that copy instead of leaving it to somebody remembering: it parses the codes the engine actually mints and reconciles them against the table in both directions, so a code added upstream (the user would see a bare code) and a code only the package believes in (a branch that can never fire) both fail. It also reconciles the table plus a small retired ledger against the engine's declared union, which is deliberately not the same set — one member was renamed and its old name is still declared — so reviving a code the engine will never mint again is impossible and a future stale member shows up immediately. Sentences are pinned one per code, mutually distinct, and split by family: a rule that is wrong and a rule that is legal but unsupported on this lane are different next steps and may not share a sentence. The engine's own message rides along as prose — sanitized and capped after escaping, never matched on |
|
|
@@ -259,7 +259,7 @@ public-surface guard checks that last one).
|
|
|
259
259
|
| `scripts/run-usage-verbatim-channel-test.mjs` | The two complementary usage disciplines (core 3.0.0 metering semantics): the CC `ModelUsage` mirror stays pure (five pinned keys, `totalInputTokens` has no seat), while the sema-owned channel forwards the engine `turn_end.usage` object **verbatim** (six keys, incl. `totalInputTokens`) via `last_turn_usage.engineUsage` / `handle.latestEngineUsage` — honest absence on pre-3.0.0 engines, no fabricated zeros |
|
|
260
260
|
| `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned. Driven against a real fake-engine HTTP server through the shipped dist |
|
|
261
261
|
| `scripts/run-shell-gate-durable-allow-test.mjs` | #110: the durable approval leg for **shell** gates. The tool_end HOLD/REJECT predicate must cover Bash the same way park detection already does (otherwise the park poison frame `Operation aborted` hits the transcript, `endedCalls` swallows the real replayed result, and the user who pressed Yes watches a command that really ran be reported as aborted); a replayed, already-decided park must resume reading the stream instead of being reported as a failed turn; `lastEventId` must track numeric `seq` too. Mutation-proven: each of the three fixes reverted turns the gate red |
|
|
262
|
-
| `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the #110 disease, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live |
|
|
262
|
+
| `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the #110 disease, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live. (6) The live-frame leg carries the fact behind "you are being asked because the auto-mode classifier could not run" all the way to the card port. Transit narrows on SHAPE only — a non-empty cause string is taken verbatim, an open set, because the word table's owner is the engine and re-checking a closed table at the package boundary would drop a legal value the day a new cause word appears, which is exactly the information worth keeping. A malformed carrier degrades to absence rather than half-minting, and absence stays absence: it covers "the classifier answered", "this ask never qualified" and "this deployment has no classifier" at once, so nothing may render it as reassurance. The guard also pins the division of labour that makes the open set safe — the same word that transits is judged again by the public display reader, which narrows to the availability axis, so a word the engine says it never stamps on this fact renders no sentence while still being visible on the card for triage |
|
|
263
263
|
| `scripts/run-park-hop-progress-test.mjs` | L-80: the park re-attach loop budgets **stalled** rounds, not parks. A turn where the model keeps hitting gates and every one of them is really decided (a card was answered, the engine really moved on) must never be cut off by the hop budget — the budget counts consecutive rounds that produced no progress, and "the engine revived and immediately parked again on the same coordinates" is not progress. The three non-progress arms (already-resolved, decide-transport-exhausted, and a re-scan that was adopted but led nowhere) share one same-cause limit instead of one arm having a limit and the others having none, and every non-progress re-attach is announced once through the host callback rather than only to the debug log. When the limit is spent the resolver reads the approval queue once more and puts whatever is decidable in front of the user before it gives up; only when there is genuinely nothing to show does it fail soft, and the terminal message then carries the real cause and a real way out instead of a sentence about a budget. On the self-heal side, a reopen verdict that reports `decidedWithoutCard` — the chain settled the gate by rule, so there was no card to present — is progress, not a reopen failure, and the user is not told their message was NOT sent. Negative control: a genuinely empty queue with a run that never moves still fails soft |
|
|
264
264
|
| `scripts/run-notif-fleet-honesty-test.mjs` | [2393] the five notification/fleet disciplines a green type-check cannot see, each proven by reverting the fix. (1) The workflow-side dedup `return` keeps a count and a trace — without it "suppressed by design" and "a real completion swallowed because the runId minting changed" are the same observation. (2) `seq` normalisation has exactly one mint point, so a 0-based or fractional wire `seq` cannot make the watcher lane and the frame lane key the same completion differently (which would feed the model twice). (3) The TTL sweep defers to a probe arm that is still inside its own deadline — an entry recorded as "abandoned" must not be delivered a moment later — while an arm that has outlived its deadline never blocks the sweep, so the headless exit gate keeps its liveness. (4) The reset hook really clears every ledger it claims to (the sticky `prompt` ledger leaked across cases). (5) The fleet ledger counts all three drop paths (malformed / unknown frame type / isolation drop), and the panel projection's settled recycling is anchored on the settle instant and skips still-present rows, so the dedup token is never carried off with the entry (which would re-emit `end`) |
|
|
265
265
|
| `scripts/run-public-surface-test.mjs` | The outward promises: the npm export surface baseline (an **exact set**, both directions — a new export that never entered the baseline is one nobody watched leave, and deleting it later would not be red), the peer floor witness, and this README's claims |
|
|
@@ -269,7 +269,7 @@ public-surface guard checks that last one).
|
|
|
269
269
|
| `scripts/run-client-core-singleton-test.mjs` | Module-level singletons ⇄ `docs/refactor/p1-scan/singleton-manifest.json`, **both directions**: an unregistered singleton is red (registering it forces someone to answer "what if this got duplicated"), a stale entry is red, and the `dupRisk: high` count only goes down |
|
|
270
270
|
| `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 |
|
|
271
271
|
| `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 |
|
|
272
|
-
| `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) |
|
|
272
|
+
| `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), and a section for the parked twin of the classifier-unavailable fact: the upstream declares that key on the parked action itself, verbatim and under the same name as the synchronous ask, so this leg reads it rather than guessing a carrier name the way the deliberately unprojected keys must. The guard drives both legs with the same cause and asserts the card ends up byte-identical either way — the observable consequence of one reader serving two key paths, and the thing that silently diverges the day someone writes a second copy. Its own reach is printed rather than implied: what is proven is the package-boundary promise "on the row ⇒ on the card", not that today's engine flattens that key onto the pending row. A further section covers the two display facts the recast had been dropping for far longer. One of them the row has carried all along under a DIFFERENT NAME than the live frame uses — the frame puts it at the top level, the row nests it under the risk descriptor — and that difference in name is exactly why it went unnoticed; unlike the keys this leg deliberately refuses to project, its carrier is witnessed in the engine's own artefact rather than guessed. Neither is decoration: the shell's stand-aside arm reads them, so a call that matched a remembered allow rule which could NOT silence it looked like an ordinary ask on the durable path and was auto-approved with no card at all. Both land on the SAME card slot the live leg uses (one shape for the ends), verbatim bytes, present only when non-blank, never folded into an empty string — and the guard pins the discipline in both directions, including that a top-level key the upstream row does not actually have must still not grow this position |
|
|
273
273
|
| `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 |
|
|
274
274
|
| `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 |
|
|
275
275
|
| `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 |
|
|
@@ -280,7 +280,7 @@ public-surface guard checks that last one).
|
|
|
280
280
|
| `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
|
|
281
281
|
| `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited |
|
|
282
282
|
| `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `isApiErrorMessage` class flag so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry |
|
|
283
|
-
| `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible |
|
|
283
|
+
| `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible. (3) The same disease growing on the REQUEST side: the `.mcp.json` → server-spec projection rebuilds each server key by key, and the settings schema deliberately leaves some keys parse-transparent — whatever JSON the file carries reaches the engine untouched, because validating them where the whole domain parses all-or-nothing would let one bad declaration take every server down silently. The whitelist had no row for the newest of them, so an operator's per-tool declarations — the ones the write fence reads — were stripped at the package boundary while both sides looked correct. The criterion is not "is that key handled" but the transparent-key table read out of the INSTALLED schema at runtime, reconciled name-for-name against this leg's ledger, so the day upstream adds a third one this turns red and forces an explicit decision. Behaviour is pinned on both transports, by object identity rather than deep equality (a rebuild would be a second judge), and malformed values must transit UNCHANGED rather than be refused here — the engine refuses them loudly and names the server, whereas a package-side judge can only swallow a declared protection quietly. Absence still mints no key, unknown keys still never reach the wire (the fix is the dropped key, not the gate), and the one transparent key this leg deliberately does not forward is a ledger entry with its own exit condition: it belongs to the deployment plane, and the day the request-plane type declares it the entry's premise is gone and the gate says so |
|
|
284
284
|
| `scripts/run-esc-halt-plan-test.mjs` | The Esc stop decision every client shares: fire the **turn-level** halt first, and escalate to a **run-level** cancel in exactly two cases — the engine itself answered with a 409 from the closed code set (it is saying "there is no in-flight turn here; use cancel for a run-level stop"), or that shot came back with no verdict at all *and* the shell can independently prove a permission card was on screen. Everything else does not escalate. The asymmetry is the whole point and every negative control guards the same direction — deciding *not* to escalate costs the user one more choice on a busy-session card (recoverable), deciding to escalate wrongly tears down a run that was alive and takes every in-flight tool with it (not). So: the closed code set is a **frozen** value, not a `ReadonlySet` — type-level immutability does not stop a consumer's `.add()`, and the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift; the escalation gate is the **conjunction** of that closed set and the 409 status, since honouring the code alone lets a 500 that merely quotes it drive a destructive call; `interrupt.not_held` and `steering.not_running` are deliberately outside the set (the first means *this replica* has no live face — the run may be perfectly alive on another); an unreadable code falls to the no-escalation side; a `parked` flag never overrides a verdict the engine did give, and only strict `true` counts when it did not. The first shot is unconditional by construction — it does not consult `parked`, because the 409 it earns is exactly the verdict the gate wants — and the verdict itself is a closed machine-readable reason word, not display copy. A third escalating case was added once tearing the stream stopped reaping the run: with detach armed, a shot that never lands leaves the run going all the way to the end of the turn, so the Esc the user pressed has no effect at all and nothing on screen says so — the old behaviour had a silent backstop (tearing the stream ended the run) and that backstop is gone. The new fact is held to the same three disciplines as `parked`: it is read only where the engine gave no verdict, it is judged **after** `parked` so an existing host's reason word does not change under it, and only strict `true` counts. Absence is proven to be a no-op rather than asserted — the guard carries its own reference implementation of the previous version's table, runs the full grid through both, requires zero divergence when the new field is omitted, and first shows the comparison really does report a difference on the one cell where the two versions are meant to differ |
|
|
285
285
|
| `scripts/run-peer-frame-projection-test.mjs` | The three engine-injected lanes design/385 puts on the **one** `task_notification` carrier, which are not the same kind of thing at all: a delegated child's uplink (`agentMessage`), another session's message drained from this session's own box (`crossSessionMessage`), and a receipt about one of *this* session's own outbound messages (`crossSessionNotice`). The engine renders none of them inside a `<task-notification>` shell, so a client that projects them as the generic completion card shows "background task finished" while the model read a colleague's sentence — two faces describing different events. The discriminator is pinned to the **typed carrier being present**, never to the `summary` text: those carriers can only be minted by the engine's injection legs (the external `notify()` input is a strict subset of the payload and can wear none of them), while `summary` is filled by every notification there is — so anchoring on text would let any background task impersonate a colleague's message by writing `<agent-message from="…">` into its own summary, and a positive control asserts exactly that payload still projects as the generic card. Fail-closed has two tiers rather than one: a broken **required** field (empty `from`, a non-string `body`, a notice `kind` outside the closed set) returns absence so the caller falls back to the generic card — an honest downgrade where the user still sees the notification — while a broken **optional** field drops only itself, because losing an attribution note and losing a colleague's whole message are not the same magnitude. The provenance side record is **required and must agree on four points** (`kind` matches the lane; `from`/`taskId`/`seq` are present and equal the carrier/payload — each equality is anchored on a core mint site and pinned by the cli wire-anchor A-K24), so a carrier signed with a trusted name but a disagreeing provenance falls back to the generic card; peer bodies pass the same authority-envelope neutralization core applies (`<task-notification>` etc. are defused) so a colleague's text can never seed the resume dedup ledger. Lane precedence copies the engine renderer's own order, because the model already read the frame in that order and a client ordering of its own would put a card on screen that disagrees with the frame the model saw. Rendering and parsing of the transcript line live in the same module and are round-tripped in both directions, including a body carrying a forged closing tag (a parser fooled there hands half a message to the next row) and a quote inside the sender label (which must not forge a second attribute); the notice lane is deliberately kept **out** of the parser, since recognising it would mean anchoring the `[Cross-session …]` prefix and a user typing that same line would be rendered as engine speech. Hostile carriers are read as own **data** descriptors only and accessors are never invoked at all — `catch` catches throwing, not never returning — proven by a counting getter that must stay at zero calls, alongside a revoked proxy and a prototype-only carrier; and four legacy payload shapes assert the no-carrier path is byte-identical to before, which is the executable form of "zero difference for an older host" |
|
|
286
286
|
| `scripts/run-wiring-manifest-projection-test.mjs` | The two end-user facts carried on the engine's `wiring_manifest` frame (`modelGate`: which tools this run's model gate removed and the verbatim restore hint; `autoMode`: whether auto mode is actually armed and the engine's own reason word). Projection: both sections ride as `_sema_`-prefixed superset keys, verbatim, and no SDK-named key is minted; a frame where neither section is well-formed projects to `none/not_in_slice` (no empty arm); `modelGate` needs all three keys and treats `removed: []` as a bad value rather than a reading; `autoMode` needs a boolean plus a non-empty reason that agrees with it, and the reason word is never mapped onto the capabilities vocabulary; the frame is flat (a nested `manifest:{}` wrapper is not a supply); `eventId` rides like every other arm. Adapter: exactly one chrome event on the main lane, a sub-flow frame (any `parentToolCallId`, `null` included) yields nothing, and an absent `eventId` leaves the key absent. Added at receiving time because the shell-side gate could not see this package's behaviour: two mutations (empty `removed` accepted, sub-flow gate removed) had passed the package suite untouched |
|
|
@@ -288,7 +288,15 @@ public-surface guard checks that last one).
|
|
|
288
288
|
| `scripts/run-resume-refusal-copy-test.mjs` | The **words** a client says when a resume is refused, minted once here instead of three times. The facts behind them already lived in this package; the sentences did not, so each client wrote its own — and those sentences answer a safety question (was my decision consumed, can this token still be redeemed), which is exactly the kind of answer that must not vary by client. Two closed sets meet here and the guard pins their relationship in both directions, because it is a premise rather than a coincidence: one set answers *can waiting help* (the codes the server mints a wait on), the other answers *what should a person be told*, they **intersect in exactly one code**, and each keeps a member the other must not have — a placement mismatch is never waitable no matter what arrives on the response, since its remedy is a changed argument rather than elapsed time, and a full governance window needs no prose because "you can wait" is the whole message. The overlapping code delegates its wait and its disposition to the existing reading rather than judging again: nine shapes of input drive both entry points and the two readings must agree byte for byte, the absent case included, because two judges always diverge somewhere. The wait is narrowed to the domain the server mints it in, which is **stricter than the shell's own copy was** — a zero now reads as no window rather than as "retry now", and the wake-up it would retry is an at-most-once action with real side effects. The third sentence is chosen by the disposition, never by the engine's prose: rewriting the message to either upstream branch's exact wording, with the window untouched, must leave all three sentences unchanged, while adding a window must change the third one and only the third one |
|
|
289
289
|
| `scripts/run-resume-retry-later-test.mjs` | The two resume refusals that carry a **wait quantity** — the only members of that refusal family that do, which is the whole reason they form a closed set. Carrying a wait is not the same as being the only ones worth waiting on: a sibling refusal in the same family clears on its own and the engine says so in words, it just cannot put a number on it, so *not recognised here* must never be read as *waiting will not help*. One of the two also has a *terminal* upstream branch that arrives under the same code with the distinguishing detail only in prose, so recognition alone is not permission to say "try again": the disposition is decided by **positive evidence** and pinned from both directions — the quota-window code is evidence in itself, the preflight code counts only when the server really supplied a wait (an upstream fact, not a convention: the terminal branch throws with no detail at all, so a wait value cannot reach the client on that path), and a preflight refusal with no wait reads as *undecidable* (say what is true of both branches — nothing was consumed — and leave redeemability to the engine's own line) rather than being rendered as either a retry or an ending. Every other member means waiting will not help (change a setting, relaunch, the retained session is gone), so the recognition is a **closed set of two codes**: widening it to a family prefix would tell half the users to wait and the other half to keep waiting for something that will never arrive, and the negative controls drive exactly those codes through it, plus a same-named code on a different door (the submission-side quota refusal), the two underscore-form siblings, and a code merely quoted inside a message body. The wait value is narrowed to the same domain the server mints it in (a whole number of seconds, at least one): zero, a negative, a fraction and a non-number all read as **no window given** rather than as zero, because a zero tells the caller to retry immediately and the wake-up it would retry is an at-most-once action with real side effects. Reading is structural rather than `instanceof`, since the client is host-injected and the same class name across two bundles is two classes, and a null-prototype plain object must still be recognised. The failure classifier gains this one disposition without any existing one moving, an unknown code still falls to the honest open-set arm and its wait value is **not** believed, and an end-to-end call proves the disposition and the window reach the host while the call itself is still attempted exactly once. The recognised code set is a **frozen array**, not a type-level readonly set: the latter is a plain mutable collection at runtime and the decision reads the same instance, so one `.add` from any consumer would turn a refusal that waiting cannot fix into one that claims it can — the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift |
|
|
290
290
|
| `scripts/run-model-capability-probe-test.mjs` | Whether a model on the OpenAI-completions lane **thinks**, and whether that thinking can be **turned off** — a question nobody can answer by looking at a model name, and one whose wrong answer costs every later call. The probe is judgement only: the network half arrives as an injected port, so the package mints no URL, reads no credential and never calls `fetch` — pinned by a source-level assertion, because a package that reaches the network once has changed what every host must trust it with. The seven dialect words are a **copy**, reconciled element-wise against the installed engine’s own bytes in both directions, since the words belong upstream and a private table drifts the day a dialect is added; the settings package deliberately declines to restate them, so the table cannot be imported from there and this guard is what stands in for the import. The **order** the dialects are tried in is a public promise rather than an implementation detail — each extra attempt is real money and real latency against someone’s gateway — so the guard pins the exact call sequence a stub records, and reversing it reds on the wasted round trip; the template-parameter spelling leads because an observed gateway keeps thinking, and answers with an empty body, when handed the top-level switch instead. That observation is also why an empty answer is **not** accepted as *thinking is off*: a knob that deletes the reply is not a knob that disabled reasoning, and accepting it would write a spelling into the catalogue that the gateway does not honour. Two dialect words whose request bytes are identical to another’s do not each burn an attempt. The two verdicts that look alike are held apart from both directions: *tried everything, still thinking* requires at least one attempt to have **cleanly answered**, and when every attempt was refused the verdict is *could not tell* instead — and on the unanswerable path the result carries **no** thinking flag at all rather than a fabricated `false`, while the pure write-back returns the very same entry object untouched. A verdict that reasoning cannot be disabled **removes** a previously declared spelling rather than leaving it, since a refuted spelling keeps the engine sending bytes the gateway ignores while the catalogue still renders it as already off. Evidence is lengths, finish positions and status codes only — a planted secret in both the answer and the reasoning channel must appear nowhere in the result, so the record can go into a log or a ticket whole One cross-package premise is checked by really running the other package’s parser rather than quoting its documentation: everything this probe writes eventually passes through the settings schema on its way into a catalogue, and that field is declared parse-transparent precisely so the vocabulary can live on the consuming side. If it ever narrows, the spelling is stripped **silently** — indistinguishable from the probe never having run — so the guard feeds the probe’s real output through the real parser, checks the compat object comes back key for key, and checks a dialect word this client has never heard of survives too. Two shapes that must be rejected really are rejected, since otherwise the survival checks would hold on a parser that accepts anything, and a bare entry is asserted valid first, because the first run of this section reddened on a space in a fixture’s name — a fixture that cannot pass would disguise the real alarm as already having fired |
|
|
291
|
-
| `scripts/run-
|
|
291
|
+
| `scripts/run-decide-receipt-test.mjs` | What a decision verb actually **answered** — and, more importantly, what it did not. A success response on the newest lane is only an acknowledgement that the decision was accepted for delivery: the approval is still pending, and a client that clears the card on it shows either a ghost card that was already approved or a card that vanished while the decision was lost. So the package deliberately has **no** "was it resolved" predicate — nothing in that body can answer it — only the opposite one, whose `false` is likewise not evidence of resolution; resolution is only ever the next running arm on the stream. The guard pins that inversion in the product source too: the success path must no longer clear the latched gate, while the stream-observing path that really clears it must still be there. The body has four shapes with **no** key common to all of them, so every position is read as honestly absent, and the handoff handle — which run to watch from here on — requires **two** facts together, since either one alone would either point the stream at the run it already had or mint an empty handle. The record of what finally happened to an already-decided action is read through the **same** reader as every other gate record rather than a second copy, and its absence means **unknown**, never *it was allowed* — the two can even contradict each other, so the card says nothing at all when it is missing. The three refusals on that lane each get one distinct sentence and a disposition taken from **why** each was refused rather than from severity: one cannot be helped by re-sending at all, one waits on the host, one just drops an option — and none of them carries a countdown, because the server never mints a wait for them. Recognition is a **closed set**: an unrecognised code on the same prefix returns nothing rather than a guess, since that prefix also houses a safety signal whose whole rule is never to retry automatically, and the recovery handle is read as absent when unreadable rather than substituted from a different identifier that no longer appears on that lane |
|
|
292
|
+
| `scripts/run-approval-frame-chrome-arms-test.mjs` | The two in-stream approval frames finally reaching every host through the shared pipeline instead of one shell's private branch — the shape of a layering defect: hosts that only consume the package could not rebuild their pending cards after a reconnect, and did not clear a card the engine had withdrawn. The payload is deliberately carried as the **envelope** the upstream types declare rather than the first-version card: the stream parser applies no predicate, so narrowing here would let a legitimately newer frame pass as the older shape and invite consumers to read keys a newer card never promised. The guard therefore pins that every open key survives untouched, that an unknown version still passes through, and that narrowing is left to the host's own predicates — with the fallback being a generic card and a person, **never** an automatic denial. A frame whose version cannot be read at all is reported as malformed rather than dropped in silence, because both frames carry user-visible decisions and state changes. Both arms are registered as **required** host duties, and their duty text names the load-bearing rules a host would otherwise have to rediscover: which predicate to narrow with, that the reconnect preamble — not a replayed historical frame — is the authority on which cards exist, and that a withdrawal frame can be lost entirely. Unlike the sibling arms, these carry **no** sub-stream cutoff: an approval raised under a delegated call still has to reach a person, and filtering it by ownership is the host's job, not a reason to discard it. Finally the upstream bytes that justify the envelope discipline are checked to still be there, since the whole design rests on them |
|
|
293
|
+
| `scripts/run-terminal-status-vocabulary-test.mjs` | One place that decides whether a run has **ended** and whether it ended badly — written because that judgement had already been hand-copied three times, so the day the engine added a word for *the agent itself reported it cannot continue*, every copy missed it and a panel settled a self-reported failure as a success. The distinction the table exists for is pinned from both sides: that word belongs in it, while the two words meaning *waiting for a person to decide* deliberately do **not** — reading those as endings would bury a run that is actively waiting on the reader. A word this client does not know answers *no*, and the guard states plainly that *no* is not evidence of success: proving success means reading the positive side, so negating this predicate is the very mistake that caused two earlier incidents. The fleet lane gets the same treatment from the other direction: a workflow parked on a durable approval used to fall through to *running*, leaving the person with no hint that a card was waiting, and it now lands on the same rendered word the task lane already used — same fact, same word, checked end to end on a real row. Why the word was added directly rather than carried as a private superset key is checked mechanically against the upstream declaration being open, so the day it closes this reds and the decision gets revisited. The residue sweep is the point: the source tree must contain **no** further inlined copy of the judgement, each of the three former sites is checked to really read the single predicate, and the one reviewed exemption carries its reason **and** a liveness assertion, so an exemption whose justification expires cannot quietly keep standing |
|
|
294
|
+
| `scripts/run-retired-vocabulary-census-test.mjs` | Whether a retirement really happened. When upstream removes a family, a downstream package can cut it out or keep a courteous alias — and the alias is the worse outcome: three clients keep writing branches for something nobody emits, and a status line advertises a state it can never reach. Choosing the clean cut only means something if a guard holds it, since a comment saying *retired* is not an exit code. Each registered entry is held two ways: the name must be gone from **code positions** in this package (comments stripped first, because the explanation is supposed to stay) and off the published surface, and — the half that keeps this from being self-congratulation — it must really be gone **upstream**, since that is the entire reason it was removed here; if it comes back, the disposition deserves reconsideration rather than silence. The scanner proves it can speak by finding a symbol that is genuinely present before any absence is believed, and distinguishes a mention inside a comment from one in a string literal, which is exactly the form being cleared. A closing check runs the other way: the retirement **story** must remain in the comments, including a promise this package made earlier and has now had to withdraw — deleting the history alongside the code is a bad way to satisfy *zero hits*, and leaves the next reader with code that has no reason |
|
|
295
|
+
| `scripts/run-classifier-status-test.mjs` | What state the auto-mode classifier is in **on this session** — the question a doctor line, a model settings page and a permission card’s status row all ask, and a different question from the one the approval card asks (*why am I being asked right now*), so the sentences are pinned mutually distinct from that face’s as well as from each other. The session-level half of this reading — a breaker record the engine used to keep — was **retired upstream**, and the guard now holds that retirement from **both** sides: the engine's own declarations must really no longer carry it (a fact coming back would mean the removal here was the wrong disposition, and that deserves a conversation rather than silence), and this package must carry no alias, no state word and no leftover narrowing for it — a reading kept alive for something nobody emits any more is a promise the interface cannot keep, and it left the doctor line advertising a state it can never reach. What remains is ordered by the quantity that actually decides whether the classifier is running: the fact from **this round** first, then whether this leg is armed — a decider is minted per run, so a later leg can be armed again. Not armed, and a section that never arrived, both answer **undefined** rather than *available*; that arming question has its own field and answering it twice grows a second ledger. Arming and availability are also **two words, not one**: the engine says a decider was minted *for this leg*, which is an assembly-time fact, while whether that decider answers any given round is a **per-call** one — so an armed leg reads `armed` and only a positive per-call fact (an ask whose origin is the classifier's own denial-bound fallback, which by construction stands *after* the classifier ran) reads `available`. Every other ask origin is refused as evidence and for a stated reason rather than out of caution: several are ones the classifier is structurally forbidden to answer, and for the rest a surviving ask is precisely the case where it did **not** resolve one — so reading availability off them would be a guess. The projection is a **whitelist**, so an older engine still sending the retired member loses it at the boundary while the two live facts beside it ride through untouched. Rendering never throws and never impersonates: a state word this client does not know — including the retired one, which a restored view can still carry — reaches an honest fallback that names it verbatim, carries no invented explanation of a mechanism that no longer exists, and is proven distinct from all three real sentences; prototype keys reach that same fallback rather than a function body, checked against a real out-of-table word so the comparison cannot hold vacuously |
|
|
296
|
+
| `scripts/run-compaction-boundary-projection-test.mjs` | The compaction divider and the one frame that makes its anchor resolvable. The trigger word is passed through as an **open set** instead of being folded to two: the engine deliberately stopped flattening its third value (a compaction that was not optional — a prompt-too-long recovery or trim pressure) and carries what the hook layer saw, so folding it again at the package boundary re-introduces exactly what upstream had just removed, while a consumer branching on *is it manual* keeps its behaviour byte for byte. Only an unreadable word (absent, empty, non-string) falls back — that is *could not read it*, not *read it and did not recognise it*. Two superset keys ride the metadata and neither fabricates: the preserved-segment anchor is minted only when its id really reads out, because half an anchor sends the host looking up an empty string in its map, and the clamp ratio is a **disclosure** whose real zero is a fact rather than an absence. The clamp ratio also carries a registered exit condition — the service really sends it while the SDK arm has no seat for it yet, so the read is defensive and this guard reds the day that seat appears, forcing a re-check instead of leaving a cast to rot. The committed-message frame moves out of *deliberately not projected*: that classification was true about transcript rows and false about **positioning**, since the engine states that consumers build their own id-to-message map from this frame to place the divider — projecting the anchor without it hands the host something it cannot resolve. It becomes a neutral internal arm and an optional chrome ledger event, never a transcript row (the frame carries no body, so minting one would put words in the engine's mouth), with both required ids narrowed and a malformed frame recorded rather than half-minted |
|
|
297
|
+
| `scripts/run-cost-absence-projection-test.mjs` | Telling **declared free** apart from **never priced**, in both directions, because the package was getting each one wrong in the opposite way. The engine separates them on the wire — an absent cost means some spend had no price table, an explicit zero means the model declared itself free — and the result projector used to require a *positive* number, so a genuinely free run could not say so; while the per-model mirror folded absence to zero, so an unpriced run told a billing consumer it cost nothing. The total is now reported as the engine stated it, with absence and non-finite values alone reading as unknown, and a negative passed through rather than corrected, since a refund is a legal figure and the package is not a second accountant. The per-model figure keeps the CC shape intact — that field is a required number and *unknown* is simply not expressible in it — so the value stays zero and a **companion superset bit** carries the distinction, which means the two are read together and a reader that only ever looked at the number is unchanged; the bit is minted only in the absent case and never as `false`, since a key present with a false value reads as a third state. The same mint point serves both the wire's per-model split and the synthesised current-model row, so neither can drift. Alongside it the cache-write figure stops being a hardcoded zero and reads the field the wire has always carried, in both the flat usage and the synthesised row, and all four flat token slots move from a null-coalesce to a finite-number guard — the stats object has an open index signature and the wire is JSON, so a string or an infinity would otherwise land in a slot the types promise is a number, compiling green and surfacing only when something sums it |
|
|
298
|
+
| `scripts/run-task-progress-terminal-projection-test.mjs` | The one tick that says a delegated child **finished**. The engine fires exactly one final beat carrying a terminal face, and says in the same breath why it exists — so a consumer sees the row finish instead of watching it vanish after the last running beat — but the package's projection whitelist had no seat for that field and its adapter still carried the older premise in a comment, so the terminal beat arrived byte-identical to another running one: the panel row stayed up waiting for a defensive sweep (which only ever settles rows bound to a card still open this turn) or for a separate notification frame. The status now rides through as an **open set** with the vocabulary left upstream, while the question *which words are terminal* is answered by a closed pair on the adapter side — an unrecognised new word takes the running path, because guessing it terminal ends a row that is still working whereas one extra running beat merely renders late. A terminal beat settles the row directly under the lane proof its binding gives it (not the main lane a notification would use, and not by card id, since the engine is naming a child rather than closing a card), freezes the inline group-row twin in the same beat so a later sweep cannot reset the real tool count, clears the session-resident ledger, and fires the stop hook only for a child whose start really fired. It does not mark the row live or emit a second progress beat, and it shares the settled-row ledger with the other two settle legs so a replay or a double-delivery cannot produce a second end. Three things are pinned **unchanged**: a running beat, an absent status (older engines never send the field, and reading absence as terminal would make every child row disappear on its first beat), and the workflow lane gate, which still runs before any of this |
|
|
299
|
+
| `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
|
|
292
300
|
|
|
293
301
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
294
302
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|