@sema-agent/client-core 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -28,6 +28,111 @@
28
28
  > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
29
  > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
30
30
 
31
+ ## 0.48.0(未发布)
32
+
33
+ 五件 additive 投影/上收批(0.48.0 提货窗:sdk **7.4.0** / server **7.54.0** / core **7.1.0**)。
34
+ 🔴 **唯一的非 additive 面 = peer 地板抬到 `@sema-agent/sdk >=7.4.0`**(见文末「地板影响面账」);
35
+ 既有键与文案逐字节不变,新增 **4** 个运行期导出(公开导出面 790 → **794**)。
36
+
37
+ - **件① `task_progress` 的 `model` cast 退役 + `GateCurrentPending` 单源化**。
38
+ 0.47.0 那两处各自写死了退役条件,本批**按条款兑现**,不是顺手整理:
39
+ - `eventToSdkMessage` 的 `const model: unknown = (ev as {model?:unknown}).model` 整条删,改类型面
40
+ 直读 —— sdk 7.4.0 已声明 `task_progress.model?: string`。**退役的是 cast,不是运行期判**:
41
+ `typeof` 门保留(旧 server 缺席 ⇒ 键不 stamp),端侧行为逐字节不变。
42
+ - `GateCurrentPending` 由**自铸 interface** 改成上游形的**别名**,自铸记账段整段删。
43
+ ⚠️ **绕道取形,原委成文**:`ApprovalStaleCurrentPending` 在 sdk 7.4.0 里
44
+ `dist/types.d.ts` 有 `export interface`,但 `dist/index.d.ts` 的 re-export 名单里**没有它**,
45
+ 且包 exports 映射只开 `.` / `./registry` ⇒ **按名 import 不通**(本批亲验)。故用
46
+ `Readonly<NonNullable<ApprovalStaleError['currentPending']>>` 取**同一个声明**,单源化目的一字不打折。
47
+ 🔴 **`Readonly<>` 不是装饰**:0.47.0 自铸形三位全 `readonly`(它们是 D-1 坐标,契约=**逐字回显、
48
+ 绝不本地重算**),而上游形的成员是**可写**的 —— 裸别名会把这条已发布的类型契约悄悄弱一档,
49
+ 且**没有任何行为门看得见**(不改变任何运行期字节)。同批铸编译期钉
50
+ `AllReadonly<GateCurrentPending>`(新 `src/typePins.ts` 原语),摘掉 `Readonly<>` 当场编译错。
51
+ 📋 上游候件:请 sdk 把该名字加进 `index.d.ts` 的 `export type {…}`(纯 additive,零行为)。
52
+ - `seq` **仍然剥**:sdk 7.4.0 虽已声明它,但本层白名单的准入条件是「说得出谁读它」,答案仍是
53
+ 零消费方。🔴 **声明到货不是透传的理由** —— 否则白名单会随上游类型面自动变宽。
54
+
55
+ - **件② durable park 行的 bidi 披露位 `hasBidiControls` 随卡透传**(S-30①,server ≥7.53 / core 5.60.0 #438)。
56
+ `PendingCheckpoint.hasBidiControls` → `ApprovalCardRequest.hasBidiControls`。病形与 [C170] 那次
57
+ `ruleSuggestions`/`governanceForced` **逐字同一个坐标**:「行 → 卡」重铸处闭形,行上新键整键丢失。
58
+ 🔴 落**新键**,**绝不并进活卡腿的 `inputHasBidi`**:两者同语义、**不同源、不同算点**
59
+ (前者 core 在 park mint 时算并反范式成 durable 列、server 读列不重算;后者 server 对帧自身序列化后的
60
+ args 现算)⇒ 同一只 ask 两条腿的在场性**可以不一致**,合流就是拿一个量冒充另一个。
61
+ 只认严格 `true`;缺席 = **没检出**,禁读成「已确认干净」。
62
+ ⭕ **另两条上游读面如实记零施工**:`ApprovalStreamEvent.hasBidiControls`(本包 feed 只把 stream
63
+ 当「变了」信号,权威列表恒来自 `list()`,该帧不经任何投影)与 `InboxRow.hasBidiControls`
64
+ (本包**没有** inbox 投影面,全仓零 `InboxRow` 引用)。
65
+
66
+ - **件③ `engine_notice` 的 raw 预分派退役 + server 7.54.0 两新码**(`task.halt_unconsumed` / `task.late_approval`)。
67
+ 🔴 **两新码的投影施工量 = 零,而这不是偷懒**:本臂按开集消费(库里一个码都不硬编),新码**按构造**
68
+ 就到得了端。真正的施工是另一件:sdk 7.4.0 已声明 `engine_notice` 臂 ⇒ 0.47.0 留的那颗**自退休钉
69
+ 本批真的响了**(devDep 抬版当拍 `tsc` 就报 `assertNeverArm` 收不下这条臂),按原定条款把 raw 预分派
70
+ 搬进 `case`,**行为一字不改**,本函数重回「每一条臂都受编译期穷举保护」。
71
+ 🔴 投影仍走 raw `Record` 视图**刻意不改吃 SDK 收窄形**:SDK 把五键记成全必填,而本层对每一键都做
72
+ 诚实缺席处理,这些分支在收窄形上会被判成死码而**静默失效**;且两个重放身份键(`eventId`/`eventSeq`)
73
+ 根本不在 SDK 臂声明里。⚠️ `text_end` 的预分派**照旧保留**(sdk 7.4.0 全树仍零命中,退役条件未满足)。
74
+
75
+ - **件④ `status`(BrainStatus)臂补 `errorStatus` **与** `retryAtMs` 两键**(core 7.0.x #506 ㋑,server ≥7.53)。
76
+ 🔴 **族扫的产物,不是只补被点名的那一个**:本批派工只点名 `errorStatus`;族扫 = 把**实装 core** 的
77
+ `BrainStatus` 整个键集与本包镜像逐一对表,当场捞出**存量**漏键 `retryAtMs`(同批进 core、同在 server
78
+ `brainStatusEventData` 白名单里真发,只是没人提)。镜像 7 键 → **9 键**。
79
+ 🔴 **同形存量清剿:这条腿上有两层白名单,两层同批修** —— `eventToSdkMessage` 的 `case 'status'`
80
+ 与 `adapt/arms.ts` 的 `retryStatusArm`;只修一层键仍到不了宿主(常驻门 G4 驱**两段合成**的全链,
81
+ 漏任一层都红)。同批**顺手退役**该臂 attempt/maxRetries/retryInMs/errClass 四位的 cast(sdk 7.4.0
82
+ 已声明这 7 键);`retryAtMs`/`errorStatus` 两位 SDK 锚仍未跟 ⇒ 保结构视图读 + 登记退役条件。
83
+ 🔴 **`deadline` 的语义与字节一字未改**:`retryAtMs` 是**产生者铸**的墙钟截止点,端在场时应优先用它
84
+ (本包现算的 `deadline` 已被跨进程跳的传输耗时推后,且在 core 每 30s 的重播片上会重新起跳);
85
+ 两位**并存**,端自己选 —— 换算法 = 一次静默的行为改动,本包不做(门 G4c 是这条方向钉的反钉)。
86
+
87
+ - **件⑤ `sessions.memoryStatus` 的三端公共读面**(S-53;新文件 `src/sessionMemoryStatus.ts`,+4 导出)。
88
+ 收在库里的理由是**两处判定**,三端各写一遍必然各错一遍:
89
+ ① **同 status 不同码** —— 本路由的 404 有两个互不相干的含义(`not_found.session` 会话未知/非属主 /
90
+ `not_found.route` <7.53 老 server 无此路由),按 **status** 分诊必然把「你的部署没这个面」说成
91
+ 「你这个会话不存在」。判据锚 `errorCode`。🔴 **无码的 404 落 `failed`(如实说判不出),绝不猜** ——
92
+ 猜错任一向都是一句用户会照着去排错的假话;501 才允许无码兜底(本路由两条 501 臂同义,无歧义)。
93
+ `capability.*` 与 `feature.*` **分列不合流**(同为 501 而处置相反)。
94
+ ② **五键缺席语义逐键不同** —— `optOutSource`/`lastCaptureAt` 在健康会话上就合法缺席,零历史会话真形 =
95
+ `{captureOptedOut:false, committedCount:0, foldedCount:0}`(**无任何降级**)。两个合读器:
96
+ `readCaptureOptOut`(两键合读,fault ⇒ `indeterminate`,绝不读成「记忆开着」)与 `readLastCapture`
97
+ (**三态**:单读 `lastCaptureAt` 判不出任何东西,判别材料是**另一键** `committedCount`)。
98
+ 纯判定 + 薄封装,IO 归宿主注入(`MemoryStatusClientLike`,与 `HitlClientLike` 同款 duck-type);
99
+ 永不抛;畸形键降缺席不采信;**空 `sessionId` 不发请求**。
100
+ 🔴 **身份绑定**:回声 `sessionId` 必须与请求值**逐字相等**,否则整只落 `failed` —— 收下不相等的
101
+ 回声 = 把**另一条会话**的记忆元数据呈现在当前会话面板上(上游同一件事的纪律是「宁缺席不串台」)。
102
+ 🔴 **矛盾形不产出确定判决**:capture 轴按**完整真值表**判(只有 `true`×`record` 与 `false`×缺席
103
+ 两格有定义,其余含 `false`×`record` / `true`×缺席 一律 `indeterminate`);lastCapture 的 `known`
104
+ 是**合取**(时刻在场 ∧ `committedCount` 是数 ∧ >0)。这些矛盾载荷版本斜差 / 畸形 200 体 / 中间层
105
+ 改写都造得出,而端拿到确定值之后不会再问第二遍 —— capture 轴尤其是**隐私姿态**断言,两个方向都危险。
106
+ 🔴 **「永不抛」成功路与失败路都设防**:归一化整段在 `try` 内;`classifyMemoryStatusFailure`
107
+ **自己**取属性也带保护 —— 它是在 `catch` **块内**被调用的,`catch` 里抛出的异常不会再被同一个
108
+ `try` 接住,分类器一抛就击穿这句承诺(同形第二处)。带抛错 getter 的对象 / 敌意 `Proxy` 作**返回体**
109
+ 或作**拒因**都落 `failed`,不向外 reject。
110
+ 🔴 **端必读**:`sessionId` 必须取**引擎捕获值** —— server 侧记录/台账按**裸 sessionId** 键控且
111
+ **活过会话**,喂一个被回收的 id 会读到**上一代**的计数/opt-out(元数据,无内容字节)。
112
+
113
+ **新增导出(4)**:`readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` /
114
+ `readLastCapture`。装 ≤0.47.0 的端按名 import 会在 ESM 实例化当场炸 —— 提货前先抬依赖。
115
+
116
+ **门**:36 套全绿(新增 1 套 `run-session-memory-status-test.mjs` **53** checks);
117
+ `additive-key-passthrough` 45 → **73** checks(新增 G3 engine_notice 开集消费 + G4 BrainStatus 两新键);
118
+ `durable-card-display-keys` 43 → **54** checks(新增 ⑪ 段 hasBidiControls 正负控);
119
+ 登记物:导出面 790 → **794** / gates 35 → **36** / portability 138 → **139** / typeshape unknown 258 → **260** /
120
+ pure BrainStatus 镜像 7 → **9** 键。
121
+
122
+ **🔴 peer 地板抬版影响面账(`@sema-agent/sdk` `>=7.2.0` → `>=7.4.0`)**:
123
+ - **为什么必须抬**(四条,任一条单独都足以钉死地板,不是顺手跟版本):① `AgentEvent.engine_notice`
124
+ 臂(<7.4.0 上 `case 'engine_notice'` 是编译错);② `task_progress.model`(cast 退役后 `ev.model`
125
+ 在旧版是编译错);③ `PendingCheckpoint.hasBidiControls`(件② 读的就是这一位);
126
+ ④ `sessions.memoryStatus` + `SessionMemoryStatus`(件⑤ 直接 `import type`)。
127
+ - **消费方要跟的**:三端装 <7.4.0 的 SDK 会看到 peer 警告;本包**运行期**不因此变化。
128
+ - **随之退役的自铸形**:`GateCurrentPending`(自铸 interface → 上游形别名)。
129
+ - **未退役、如实记**:`text_end` 的 raw 预分派(sdk 7.4.0 仍零命中);`status` 臂的
130
+ `retryAtMs`/`errorStatus` 两位仍走结构视图读(SDK 锚未跟);`ApprovalStaleCurrentPending`
131
+ 按名 import 仍不通(上游 re-export 缺口,已登记候件)。
132
+ - **core devDep 5.57.0 → 7.1.0 是 devDep-only**(core 不在 peerDependencies 里),它是 engine-vocab
133
+ 门的对账锚,**对消费方零要求**。同批亲验:`BrainStatusPhase`(6 相)与 `BrainRetryErrClass`(6 桶)
134
+ 两版**逐词未变** ⇒ 本次提级的行为面影响只有 `BrainStatus` 键集这一处。
135
+
31
136
  ## 0.47.0(2026-08-31)
32
137
 
33
138
  三件 additive 上收/透传批(#363;取证与定谳出自同批 CLI 提货)。**零 BREAKING**:
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.47.0
38
+ **Version:** 0.48.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
@@ -65,7 +65,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
65
65
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
66
66
  five modules, and the browser bundle really bundles the SDK through (the portability guard would
67
67
  exit 3 rather than quietly mark it external).
68
- - The declared floor is `>=7.2.0`, and it is *witnessed*: the guard checks that an actually
68
+ - The declared floor is `>=7.4.0`, and it is *witnessed*: the guard checks that an actually
69
69
  installed SDK at that line still exports every value-level symbol this package imports and still
70
70
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
71
71
  promise, not a contract.
@@ -250,6 +250,7 @@ public-surface guard checks that last one).
250
250
  | `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 |
251
251
  | `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op |
252
252
  | `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
253
+ | `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 |
253
254
  | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never appear in a published artefact. The guard first proves each pattern still bites on a constructed sample (a screen that cannot fail is worse than none) and honours a per-pattern allow-list for legitimate product vocabulary, so the verdict is "clean surface", not "quiet grep". |
254
255
  | `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T | null`, the `installHost` family returns `T | undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report |
255
256
  | `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
@@ -751,6 +751,16 @@ const retryStatusArm = function* (m, { ctx, flags }) {
751
751
  // 七键(#307 S44,2026-08-19):core 5.43.0 起 `BrainStatus` 带 `errClass`(等待原因分桶)。
752
752
  // 剥掉它 = 用户看得到「在等」看不到「为什么等」,而供给方是发了的。
753
753
  ...(typeof m.errClass === 'string' ? { errClass: m.errClass } : {}),
754
+ // ── 九键(0.48.0;core 7.0.x #506 ㋑,server ≥7.53 `brainStatusEventData` 两腿全发)────
755
+ // 🔴 与上面那条 errClass 是**同一个病形的第三、第四例**:上游真发、本层这层白名单闭形剥掉。
756
+ // 族扫真源 = 实装 core 的 `BrainStatus` 整个键集(engine-vocab G2-c 逐键等值门),
757
+ // 不是「派工单点名了哪一个」——本批点名的只有 errorStatus,retryAtMs 是族扫捞出的存量漏键。
758
+ // · `retryAtMs` 产生者铸的墙钟截止点(端渲倒计时的权威锚;剥掉它端只能自己 now+剩余量
759
+ // 现算,而那个算法在每个 30s 重播片上重新起跳 —— core 顶注点名的病)。
760
+ // · `errorStatus` 刚失败那次尝试的 HTTP 状态(CC `system/api_retry.error_status` 同一个数;
761
+ // 剥掉它 = 用户看得到「在重试」却看不到「谁失败了/该不该等」)。
762
+ ...(typeof m.retryAtMs === 'number' ? { retryAtMs: m.retryAtMs } : {}),
763
+ ...(typeof m.errorStatus === 'number' ? { errorStatus: m.errorStatus } : {}),
754
764
  }, ctx.now());
755
765
  // `status === null` = 引擎直报 `recovered`(重试成功)。这一拍是**清除腿之一** —— 但间接腿
756
766
  // (TurnFlags 的「主 lane 内容帧到达即清」)可能已经清过了:已安装 core 在内容流跑完的 finally
@@ -91,24 +91,6 @@ function assertNeverArm(_ev) {
91
91
  * 旧写法 `if (msg)` 在新返回型上恒真(对象永远 truthy),所以这是**必须点名**的一类改动。
92
92
  */
93
93
  export function eventToSdkMessage(ev, ctx) {
94
- // ── `engine_notice` raw 预分派(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)──
95
- //
96
- // 🔴 **为什么是 raw 预分派而不是一条 `case`**(与 `workflow_complete` / `human_input` 当年同因):
97
- // 本臂**还没进已发布 SDK 的 `AgentEvent` union**。sdk 仓 `3d6aebc` 确实加了它,但那个 commit
98
- // **尚未出包** —— 亲验 npm `@sema-agent/sdk@7.2.0`(latest,2026-08-16 发布)的真 tarball:
99
- // `dist/` 全树零 `engine_notice`(而同批的 `FleetTaskRow.cycleSeq` 在,证明抽检会说话)。
100
- // 在这样的 union 上写 `case 'engine_notice'` 是编译错,所以先走预分派。
101
- // 🔴 **这不是「按源码将就接」**(接入文档宪法):消费契约取自 server 的**已发布**接入档
102
- // (ASSISTANT-WIRE-CONTRACT 附录 D,server 7.36+)与 openapi `Event_engine_notice`,不是抄 sdk src。
103
- // 档与实装的失真已如实记账(附录 D.3 仍写「起步白名单三码」,而 server main 的白名单已是六码 ——
104
- // `memory.hold_opened` / `hold_released` / `hold_disposed` 随 core 5.47/5.48 的 `NOTICE_AUDIENCE`
105
- // 入册)。**本层对此完全免疫**:白名单是 server 的投递判定,本层按开集消费,一个码都不硬编。
106
- // 🔴 **到期复核(自退休,不靠人记)**:预分派用 `(ev as {type?:unknown})` 形读判别键,**不收窄** `ev`
107
- // ⇒ 臂一进 union,switch 的 `default` 仍看得见它,B5 穷举断言 `assertNeverArm` **编译期真红**,
108
- // 逼下一棒把它搬进 switch。搬进去时行为一字不改(下面的投影函数原样复用)。
109
- if (ev.type === 'engine_notice') {
110
- return engineNoticeProjection(ev, ctx);
111
- }
112
94
  // ── `text_end` raw 预分派(#323 / core #447,core ≥5.63 / server ≥7.50;契约见 textEndProjection)──
113
95
  //
114
96
  // 🔴 **为什么是 raw 预分派而不是一条 `case`**(与 `engine_notice` 当年逐字同因):本臂**还没进
@@ -348,13 +330,29 @@ export function eventToSdkMessage(ev, ctx) {
348
330
  // 键集真源 = retryStatus.ts 的 `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对账)。
349
331
  // #307 S44(2026-08-19):core 5.43.0 又加了 `errClass`(这次等待的**原因分桶**,
350
332
  // provider 中立闭集)—— SDK 的 `status` 臂类型同样还没跟,同款 unknown 读 + 串窄化透传。
351
- const st = ev;
333
+ // 🔴 **四键的 cast 已退(0.48.0)**:上面那段说的「SDK 的 `status` 臂类型只声明
334
+ // phase/detail/retryInSec」在 sdk 7.4.0 上**不再成立** —— 该臂现已声明 **7 键**
335
+ // (phase/detail/retryInSec/retryInMs/attempt/maxRetries/errClass,`dist/events.d.ts` 直读)。
336
+ // peer 地板同批抬到 >=7.4.0 ⇒ attempt/maxRetries/retryInMs/errClass 四位改类型面直读。
337
+ // 退役的是 **cast**,不是运行期窄化:`num()` / 非空串门全部保留(wire 是 JSON,旧 server
338
+ // 仍可能发别的形;类型声明是上游承诺,不是本层前提)。
339
+ // 🔴 **两位仍走结构视图读**(0.48.0 新增,**同形第三、第四例**):core 7.0.x #506 ㋑ 起
340
+ // `BrainStatus` 还带 `retryAtMs`(产生者铸的墙钟截止点)与 `errorStatus`(刚失败那次尝试的
341
+ // HTTP 状态),server ≥7.53 的 `brainStatusEventData` **两键全发**(7.54.0 dist 真字节直证),
342
+ // 而 **sdk 7.4.0 的 `status` 臂尚未声明这两位** —— 与 `model`(#363)/`requiresRealApproval`
343
+ // (#283)/`ruleOffers`(#334)同形:server 已真发、SDK 锚未跟。照那几例的先例**先追 server
344
+ // 不等 SDK**,类型面缺席不等于 wire 上缺席。
345
+ // 📋 **退役条件**:sdk 把这两位补进 `status` 臂声明的那一批,把它们并进上面的类型面直读、
346
+ // 删掉本段的 `st2` 结构视图(与本批刚兑现的四键退役逐字同一条路)。
347
+ const st2 = ev;
352
348
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
353
- const attempt = num(st.attempt);
354
- const maxRetries = num(st.maxRetries);
355
- const retryInMs = num(st.retryInMs);
349
+ const attempt = num(ev.attempt);
350
+ const maxRetries = num(ev.maxRetries);
351
+ const retryInMs = num(ev.retryInMs);
352
+ const retryAtMs = num(st2.retryAtMs);
353
+ const errorStatus = num(st2.errorStatus);
356
354
  // 非空串才透传(空串既不是桶也不是「不知道」,只会在下游被渲成一个空的原因)。
357
- const errClass = typeof st.errClass === 'string' && st.errClass.length > 0 ? st.errClass : undefined;
355
+ const errClass = typeof ev.errClass === 'string' && ev.errClass.length > 0 ? ev.errClass : undefined;
358
356
  return projected(stamp(ctx, armBody({
359
357
  type: 'retry_status',
360
358
  phase: ev.phase,
@@ -364,6 +362,14 @@ export function eventToSdkMessage(ev, ctx) {
364
362
  ...(attempt !== undefined ? { attempt } : {}),
365
363
  ...(maxRetries !== undefined ? { maxRetries } : {}),
366
364
  ...(errClass !== undefined ? { errClass } : {}),
365
+ // ── 0.48.0 跟车两位(core 7.0.x #506 ㋑ / server ≥7.53 两腿全发)────────────────────
366
+ // 🔴 **这一处是族扫的第二个坐标,不是第一个**:本批的病形(上游真发、闭形白名单剥掉)在
367
+ // `status` 这条腿上有**两层**白名单 —— 本层(wire → 内部 retry_status 臂)与
368
+ // `adapt/arms.ts` 的 `retryStatusArm`(内部臂 → RetryStatus)。只修其中一层,键仍到不了
369
+ // 宿主([same-shape-residue-constitution]:只修当格 = 违宪)。两层同批修,门 G4 驱的是
370
+ // **全链**(adapt 入口 → chrome 事件),所以漏任一层它都红。
371
+ ...(retryAtMs !== undefined ? { retryAtMs } : {}),
372
+ ...(errorStatus !== undefined ? { errorStatus } : {}),
367
373
  // 🔴 §E2 lane 身份必须透传(2026-08-08 对抗复审二轮复审命中的**跨 lane 状态破坏**)。
368
374
  // `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
369
375
  // `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
@@ -400,8 +406,13 @@ export function eventToSdkMessage(ev, ctx) {
400
406
  // · `taskType` / `status` / `parentTaskId` —— 早已在册的 §7 缺口 **P-1**(lane 归属今天改用
401
407
  // id 形状 / `workflowRunId` 启发式判,见 `src/adapt/arms.ts`);
402
408
  // · `seq` —— core #258 的 stop-cycle 代际号(复活即 +1)。fleet 面已有同轴的
403
- // `wireCycleSeq`(0.38.0),tick 这条腿今天**没有**消费方;SDK 7.3.0 的 `task_progress` 臂
404
- // 连声明都没有(core/server 两侧都有)。
409
+ // `wireCycleSeq`(0.38.0),tick 这条腿今天**没有**消费方。
410
+ // ⚠️ **就地订正(0.48.0)**:本条 0.47.0 的原文写着「SDK 7.3.0 的 `task_progress` 臂连声明
411
+ // 都没有」—— sdk **7.4.0 已声明** `seq?: number`(与 `model` 同批补上,JSDoc 逐字点名
412
+ // core 5.36.0 #258 两铸点)。⇒ **剥它的理由换了一条,但仍然剥**:剥的判据从来不是「SDK 没
413
+ // 声明」(那只是当时顺带成立的第二个事实),而是本白名单的准入条件 ——「说得出谁读它、读来
414
+ // 干什么」。今天这条腿的答案仍是零消费方 ⇒ 照旧不透,P-1 族账不动。声明到货**不是**透传的
415
+ // 理由,否则这张白名单会随上游类型面自动变宽,准入条件形同虚设。
405
416
  // · `eventId` —— EventIdentity 的另一半。`status` 臂 2026-08-08 已因跨 lane 状态破坏补齐两键,
406
417
  // 本臂只补了 `parentToolCallId`(lane 判据要它),`eventId` 至今无消费方。
407
418
  // 上面三档的共同纪律:要透哪一位,先说得出**谁读它、读来干什么**,并同批更新 §3d 的键级剥离账。
@@ -429,9 +440,14 @@ export function eventToSdkMessage(ev, ctx) {
429
440
  // 🔴 病形与 `workflowRunId` **同形第二例**([1616]/[1617]②):这层白名单是闭形的,于是一条
430
441
  // **已经在 wire 上**的事实到不了宿主,而两边代码看着都对(「送到了但没人读」的精确坐标
431
442
  // 其实是「没转」)。additive 透传,旧 server 缺席 ⇒ 键不 stamp,形状逐字节不变。
432
- // 🔴 **结构视图读**:sdk 7.3.0(今天 npm 上的最新版)的 `task_progress` 臂**尚未声明**这一位
433
- // —— `requiresRealApproval`(#283)/ `ruleOffers`(#334)同形:**server 已真发、SDK 锚未跟**。
434
- // 照那两例的先例先追 server 不等 SDK;SDK 补上当天这处 cast 可整条删掉(退役条件)
443
+ // 🔴 **cast 已退(0.48.0,退役条件兑现)**:0.47.0 这一行是 `const model: unknown =
444
+ // (ev as { model?: unknown }).model`,理由 = sdk 7.3.0 的 `task_progress` 臂尚未声明这一位
445
+ // (与 `requiresRealApproval` #283 / `ruleOffers` #334 同形:**server 已真发、SDK 锚未跟**),
446
+ // 头注同批写死了退役条件「SDK 补上当天这处 cast 可整条删掉」。sdk **7.4.0 已声明**
447
+ // `model?: string`(`dist/events.d.ts` task_progress 臂,JSDoc 逐字点名 server ≥7.53 /
448
+ // core 7.0.1 两处无条件铸点)+ 本包 peer 地板同批抬到 `>=7.4.0` ⇒ 按条款兑现,改类型面直读。
449
+ // 退役的是 **cast**,不是运行期判:`typeof` 门保留(旧 server 缺席 ⇒ 键不 stamp;
450
+ // wire 是 JSON,类型声明是上游承诺不是本层前提)。
435
451
  // 🔴 条件 spread 与 server 投影**同形同条件**:`string` 且非空才带。空串既不是模型 id 也不是
436
452
  // 「不知道」,透下去只会在面板上渲成一个空模型名([honest-absence-not-fabricated-zero])。
437
453
  const model = ev.model;
@@ -654,6 +670,24 @@ export function eventToSdkMessage(ev, ctx) {
654
670
  */
655
671
  case 'approval_request':
656
672
  return dropped('unsupported_arm', 'approval_request');
673
+ // ── `engine_notice`(#310 / #318 件①,server ≥7.36,契约 = ASSISTANT-WIRE-CONTRACT 附录 D)────
674
+ // 🔴 **到期复核已兑现(sdk 7.4.0 提货,0.48.0)** —— 与 `human_input`(sdk 6.9.0)逐字同一条路。
675
+ // 本臂此前是 switch **之前**的一条 raw 预分派,理由 = 它还没进已发布 SDK 的 `AgentEvent`
676
+ // union(亲验 7.2.0 真 tarball `dist/` 全树零 `engine_notice`)。sdk **7.4.0 已声明该臂**
677
+ // (`dist/events.d.ts`:`{type:"engine_notice"; code; message; detail; sessionId; ts}`,
678
+ // 五键**全必填**、无 `& EventIdentity`)⇒ 按原定的自退休条款搬进 switch。
679
+ // 这不是可选的整理:预分派用的是 `(ev as {type?:unknown})` 形、**不收窄** `ev`,所以臂一进
680
+ // union,`default` 就还看得见它 ⇒ B5 穷举断言 `assertNeverArm` **编译期真红**(本批实翻:
681
+ // devDep 抬到 7.4.0 的当拍 `tsc` 就是这一条错)。搬进来后本函数重回「每一条臂都受编译期
682
+ // 穷举保护」,行为**一字不改**(下面的投影函数原样复用,连入参的 raw 视图形都不动)。
683
+ // 🔴 **投影仍走 raw `Record` 视图,不改吃 SDK 收窄形**(刻意,不是省事):SDK 的臂声明把五键
684
+ // 记成**全必填**,而本层对每一键都做诚实缺席处理(`code` 空 ⇒ malformed;`message`/`detail`
685
+ // 坏 ⇒ 降级不丢帧;`sessionId`/`ts` 非法 ⇒ 不 stamp),这些分支在收窄形上会被类型面判成
686
+ // 死码而**静默失效** —— wire 是 JSON,必填是 server 的承诺不是本层的前提
687
+ // ([jsdoc-untrusted-verify-implementation])。且 `eventId` / `id`(→ `eventSeq`)两个重放
688
+ // 身份键**根本不在** SDK 臂声明里(SDK 从 SSE `id:` stamp),收窄形上读它们是编译错。
689
+ case 'engine_notice':
690
+ return engineNoticeProjection(ev, ctx);
657
691
  default:
658
692
  // B5 穷举:SDK union 加成员必须在**编译期**打红这一行(而不是让新臂在用户面静默丢帧)。
659
693
  assertNeverArm(ev);
@@ -65,7 +65,7 @@
65
65
  * vocabulary, no claude.ai permission-rule destinations, no widget/glyph/keybinding crosses this seam. The
66
66
  * backend supplies the SIGNAL; the shell owns the chrome.
67
67
  */
68
- import type { AgentEvent, ApprovalDecision, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
68
+ import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
69
69
  /** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
70
70
  export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
71
71
  /** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
@@ -168,24 +168,32 @@ export declare class HitlSafetyError extends Error {
168
168
  * 🔴 **恒不含 `checkpointToken`**(server 侧 resume 凭证不外发纪律)——本形也刻意没有那一位:
169
169
  * 多一个位就是给一个「凭证能过境」的错觉。
170
170
  *
171
- * ── 为什么本包自铸这个形(记账,不是偷懒)────────────────────────────────────────────────
172
- * `@sema-agent/sdk` **7.3.0** 已有逐字同形的 `ApprovalStaleCurrentPending`(`dist/types.d.ts`)+
173
- * `ApprovalStaleError.currentPending` 第 5 构造参。但本包的 peer 地板是 **>=7.2.0**,那一版上这两个
174
- * 名字都不存在 ⇒ `import type` 会让**装 7.2.0 的端当场编不过**,而抬地板不是 additive(它是对所有
175
- * 消费方提要求)。⇒ `RuleOffer`(#334)同款处置:**自铸形 + 记账**,名字刻意**不同名**
176
- * (`GateCurrentPending` `ApprovalStaleCurrentPending`),免得地板线下出现一个与上游同名却不同源的
177
- * 形([same-name-different-meaning-crosses-layers])。
178
- * 🔴 **退役条件**:本包 peer 地板抬到 `>=7.3.0` 的那一批,把本形改成
179
- * `import type { ApprovalStaleCurrentPending } from '@sema-agent/sdk'` 的别名并删掉这段记账。
171
+ * ── 自铸形已退役,本名现在是上游形的**别名**(0.48.0)──────────────────────────────────
172
+ * 0.47.0 这里是一个**自铸 interface** + 一段记账:`@sema-agent/sdk` 7.3.0 起已有逐字同形的
173
+ * `ApprovalStaleCurrentPending`,但当时本包 peer 地板是 `>=7.2.0`,那一版上这个名字不存在 ⇒
174
+ * `import type` 会让装 7.2.0 的端当场编不过。记账段同批写死了退役条件:「peer 地板抬到 `>=7.3.0`
175
+ * 的那一批,把本形改成 `import type` 的别名并删掉这段记账」。
176
+ * 本批(0.48.0)peer 地板抬到 **`>=7.4.0`**(四件新投影都吃 7.4.0 的类型面)⇒ 条件满足,兑现。
177
+ * 🔴 **别名而不是删名**:`GateCurrentPending` 是 `FsApprovalOutcome` / `GateOutcome` 两条腿的**已发布
178
+ * 导出名**,删它是 BREAKING。别名让上游形成为**唯一真源**(上游改一个位,本包的两条腿当天跟着
179
+ * 动,不再有第二份可能漂的声明),而本名继续对已接入的端有效。
180
+ * 🔴 **同名不同源的风险随之消失**:0.47.0 刻意取了个**不同名**(免得地板线下出现一个与上游同名却
181
+ * 不同源的形,[same-name-different-meaning-crosses-layers]);现在两者是同一个形,那条顾虑不再
182
+ * 适用 —— 但名字**不回改**成上游名,理由见上一条(已发布导出名)。
183
+ *
184
+ * 🔴 **为什么绕道 `ApprovalStaleError['currentPending']` 而不是直接 `import type
185
+ * { ApprovalStaleCurrentPending }`(上游缺口,本批亲验实证)**:那个名字在 sdk 7.4.0 里**声明了
186
+ * 但没有从包入口导出** —— `dist/types.d.ts:2359` 有 `export interface ApprovalStaleCurrentPending`,
187
+ * 而 `dist/index.d.ts` 的 re-export 名单里**没有它**(同文件只导出了 `ApprovalStaleError`),
188
+ * 且包 `exports` 映射只开 `.` / `./registry` 两个子路径,深引 `@sema-agent/sdk/dist/types.js`
189
+ * 这条路根本不通。⇒ 按名 import 是**编译错**,不是风格选择。
190
+ * 绕道取的是**同一个声明**(`currentPending?: ApprovalStaleCurrentPending | undefined`,
191
+ * `NonNullable` 剥掉可选位),所以单源化的目的一字不打折:上游改那个形,本包当天跟着动。
192
+ * 📋 **上游候件(已登记,不在本批修)**:请 sdk 把 `ApprovalStaleCurrentPending` 加进 `index.d.ts`
193
+ * 的 `export type {…}` 名单(纯 additive,零行为)。到货后本别名可改回按名 import,
194
+ * **但那是等价重构、不是修复** —— 今天这条路已经拿到上游形本身,不存在第二份可能漂的声明。
180
195
  */
181
- export interface GateCurrentPending {
182
- /** 当前 pending 被门住的工具名(与 pending 行 `toolName` 同源同值)。UNTRUSTED-for-display。 */
183
- readonly toolName: string;
184
- /** 当前 pending 的 `pendingAction.toolCallId` —— 壳重定位后 decide 要**逐字回显**的 D-1 锚。 */
185
- readonly boundCallId: string;
186
- /** server 铸的 `boundInputHash`(行上有才带)。🔴 逐字回显,**绝不本地重算**(§9.1 同一条铁律)。 */
187
- readonly boundInputHash?: string;
188
- }
196
+ export type GateCurrentPending = Readonly<NonNullable<ApprovalStaleError['currentPending']>>;
189
197
  /**
190
198
  * 从一次 decide 失败(任意抛出物)里读出 {@link GateCurrentPending} —— 读不出即 `undefined`。
191
199
  *
@@ -194,8 +202,10 @@ export interface GateCurrentPending {
194
202
  * SDK 的 `ApprovalStaleError` 由**宿主**决定 —— web/desktop 完全可以注入自己的传输层。拿
195
203
  * `instanceof` 当闸,等于让「宿主用的是哪一份 SDK 实例」决定一个纯读值的位在不在
196
204
  * (跨 realm / 双实例 = `HitlSafetyError` 判型契约踩过的同一个坑,见 §4e)。
197
- * ② **类型面**:`ApprovalStaleError.currentPending` 只在 sdk ≥7.3.0 上声明,而本包地板是 7.2.0
198
- * (理由见 {@link GateCurrentPending} 的记账段)。
205
+ * ② **类型面**(0.48.0 就地订正):0.47.0 这条写的是「`ApprovalStaleError.currentPending` 只在
206
+ * sdk ≥7.3.0 上声明,而本包地板是 7.2.0」——**地板已抬到 `>=7.4.0`,那半条不再成立**。
207
+ * 但本条的**结论不变**,因为承重的从来是 ① 那半:即使类型面现在够得着,抛出来的对象仍由宿主
208
+ * 的传输层决定,`instanceof` 依旧是错的判据。⇒ 保留宽读,只订正理由。
199
209
  * 🔴 **判据锚在「值本身合不合形」,不锚它骑在哪个类/哪个 errorCode 上**
200
210
  * ([anchor-on-the-deciding-quantity]):真正决定「壳能不能一跳重定位」的量,是**这个三元组的
201
211
  * 两件必填位在不在且是非空串** —— 而不是错误的类名。宽读在这里是**方向安全**的:本位不参与
@@ -53,6 +53,18 @@ export class HitlSafetyError extends Error {
53
53
  this.name = 'HitlSafetyError';
54
54
  }
55
55
  }
56
+ /**
57
+ * 🔴 **编译期负控**(对抗复审 [medium] 采纳,0.48.0):三位必须**全 `readonly`**。
58
+ *
59
+ * 0.47.0 的自铸形三位逐个写着 `readonly`,而上游 `ApprovalStaleCurrentPending` 的成员是**可写**的
60
+ * —— 首版别名(`NonNullable<…>`,不带 `Readonly<>`)把这条已发布的类型契约悄悄弱了一档:
61
+ * 消费方从此可以直接改写 `boundCallId` / `boundInputHash`,而那两位的契约恰恰是
62
+ * **逐字回显、绝不本地重算**(§9.1;本地重算一次就会把一次合法审批 fail-close 掉)。
63
+ * 🔴 这类退化**没有任何行为门看得见**(它不改变任何运行期字节)—— 名字在、字段在、行为门全绿。
64
+ * 所以钉必须下在编译期:`Readonly<>` 哪天被顺手摘掉,这一行当场编译错。
65
+ */
66
+ const _gateCurrentPendingReadonlyPin = true;
67
+ void _gateCurrentPendingReadonlyPin;
56
68
  /**
57
69
  * 从一次 decide 失败(任意抛出物)里读出 {@link GateCurrentPending} —— 读不出即 `undefined`。
58
70
  *
@@ -61,8 +73,10 @@ export class HitlSafetyError extends Error {
61
73
  * SDK 的 `ApprovalStaleError` 由**宿主**决定 —— web/desktop 完全可以注入自己的传输层。拿
62
74
  * `instanceof` 当闸,等于让「宿主用的是哪一份 SDK 实例」决定一个纯读值的位在不在
63
75
  * (跨 realm / 双实例 = `HitlSafetyError` 判型契约踩过的同一个坑,见 §4e)。
64
- * ② **类型面**:`ApprovalStaleError.currentPending` 只在 sdk ≥7.3.0 上声明,而本包地板是 7.2.0
65
- * (理由见 {@link GateCurrentPending} 的记账段)。
76
+ * ② **类型面**(0.48.0 就地订正):0.47.0 这条写的是「`ApprovalStaleError.currentPending` 只在
77
+ * sdk ≥7.3.0 上声明,而本包地板是 7.2.0」——**地板已抬到 `>=7.4.0`,那半条不再成立**。
78
+ * 但本条的**结论不变**,因为承重的从来是 ① 那半:即使类型面现在够得着,抛出来的对象仍由宿主
79
+ * 的传输层决定,`instanceof` 依旧是错的判据。⇒ 保留宽读,只订正理由。
66
80
  * 🔴 **判据锚在「值本身合不合形」,不锚它骑在哪个类/哪个 errorCode 上**
67
81
  * ([anchor-on-the-deciding-quantity]):真正决定「壳能不能一跳重定位」的量,是**这个三元组的
68
82
  * 两件必填位在不在且是非空串** —— 而不是错误的类名。宽读在这里是**方向安全**的:本位不参与
@@ -415,6 +415,26 @@ export interface ApprovalCardRequest {
415
415
  * `renderUntrustedCommandText`(core 导出的展示基线)。本包只保证这一位到得了卡口。
416
416
  */
417
417
  inputHasBidi?: true;
418
+ /**
419
+ * **S-30①(0.48.0;server ≥7.53 / core 5.60.0 #438;sdk 7.4.0 起在 `PendingCheckpoint` 上声明)**
420
+ * —— 原样来自 **durable park 行** `PendingCheckpoint.hasBidiControls`:这条 park 行的**执行载荷**里
421
+ * 含至少一个 DIRECTIONAL 格式控制符(Trojan Source:人眼读到的顺序 ≠ 真正执行的字节顺序)。
422
+ *
423
+ * 🔴 **与 {@link inputHasBidi} 刻意分键不合流**(上游把名字取得不同,正是为了不让人合并):
424
+ * · {@link inputHasBidi} = **同步腿**(`tool_approval` 活卡帧),server 对**帧自身序列化后的 args**
425
+ * 现算(E-14);
426
+ * · 本键 = **durable 腿**,值**完全由 core 供**(park mint 时 `PendingAction.hasBidiControls`,
427
+ * 反范式成 durable 行的 `has_bidi_controls` 列;server `listPending` 读列 `=== 1` 才铸,**不重算**)。
428
+ * 两者是**同语义、不同源、不同算点**的两个量 ⇒ 同一只 ask 的两条腿在场性**可以不一致**,合成一位
429
+ * 就是拿一个量冒充另一个([same-name-different-meaning-crosses-layers])。端要渲一个徽标的话,
430
+ * 读**两位的并**是允许的(那是端的呈现决定),但两位在本层必须各自到货。
431
+ * 🔴 **缺席绝不折成 `false`**(类型 `true`,与 {@link governanceForced} / {@link inputHasBidi} 同族):
432
+ * 缺席 = **没检出**(干净 / core 有界扫描没够着 / 列诞生前 park 的老行),消费端**禁**读成
433
+ * 「已确认干净」—— 那是对用户下一个证不出的断言。
434
+ * 🔴 **披露位,不是清洗位;本包字节零改**:清洗会改掉即将被执行的那串字节(卡上显示的与真跑的不是
435
+ * 同一个东西),比不披露更坏。显形(转义/高亮/加标记)归端。
436
+ */
437
+ hasBidiControls?: true;
418
438
  /**
419
439
  * 被越级的持久 allow 规则**原文**(#144,原样来自 {@link ToolApprovalFrame.persistedRuleShadowed}
420
440
  * 的合形值)——壳据此渲「你的规则仍在,只是这次调用被要求逐次确认」;缺席 = 卡形与 0.27.0
@@ -295,6 +295,13 @@ parkGatedCallId) {
295
295
  callKey,
296
296
  ...(signal ? { signal } : {}),
297
297
  ...(pending.governanceForced === true ? { governanceForced: true } : {}),
298
+ // S-30①(0.48.0):durable 行的 bidi 披露位随卡透传。**同一个病形的第 N 例** —— 这处「行 → 卡」
299
+ // 重铸是闭形的,server ≥7.53 起行上就有的这一位在这里整键丢失([C170] 那次 ruleSuggestions /
300
+ // governanceForced 逐字同一个坐标)。本批由 durable-card 门 ⑦ 段的**富行键集普查**当天抓出
301
+ // (红文逐字:「无账:hasBidiControls」),不是人眼发现的。
302
+ // 🔴 条件 stamp **只认严格 true**,与 governanceForced / inputHasBidi 同族:上游契约是
303
+ // 「`true` 或缺席,恒不写 false/null」,这里认宽了就会把一个 falsy 值渲成披露。
304
+ ...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
298
305
  ...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
299
306
  ...(isWireRecordCarrier(durableProbeCause) ? { probeCause: durableProbeCause } : {}),
300
307
  // #348(0.44.0)durable 腿的对偶:行上**本来就存着**这个值(server `parked-decide.ts`;SDK
package/dist/index.d.ts CHANGED
@@ -138,6 +138,7 @@ export * from './notifications.js';
138
138
  export * from './steering.js';
139
139
  export * from './diagnostics.js';
140
140
  export * from './retryStatus.js';
141
+ export * from './sessionMemoryStatus.js';
141
142
  export * from './adapt.js';
142
143
  export * from './subagentContentStore.js';
143
144
  export * from './engineAgentPanelStore.js';
package/dist/index.js CHANGED
@@ -138,6 +138,12 @@ export * from './notifications.js';
138
138
  export * from './steering.js';
139
139
  export * from './diagnostics.js';
140
140
  export * from './retryStatus.js';
141
+ // ── S-53(0.48.0):会话记忆姿态的三端公共读面 ────────────────────────────────────────────────
142
+ // 收在库里的理由是**两处判定**(见文件头):① 同 status 不同码 —— 本路由的 404 有两个互不相干的
143
+ // 含义(`not_found.session` 会话未知 / `not_found.route` 老 server 没这条路由),按 status 分诊必然
144
+ // 把「部署没这个面」说成「你这个会话不存在」;② 五键缺席语义**逐键不同**,健康会话上就有两键
145
+ // 合法缺席,一律读成「没有/关着/0」就是对用户下一个证不出的断言。
146
+ export * from './sessionMemoryStatus.js';
141
147
  export * from './adapt.js';
142
148
  // ── B1 批:纯函数 / 侧信道台账 / 投影闸(2026-07-27)──────────────────────────────────────────
143
149
  export * from './subagentContentStore.js';
@@ -10,7 +10,8 @@
10
10
  * gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
11
11
  * 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
12
12
  * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
13
- * maxRetries / errClass(core 5.43.0 起七键)。
13
+ * maxRetries / errClass / retryAtMs / errorStatus(core 7.0.x 起**九键**;0.48.0 补齐后两位,
14
+ * 族扫账见 {@link BRAIN_STATUS_PAYLOAD_KEYS} 末段)。
14
15
  *
15
16
  * 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
16
17
  * (core `BrainStatusPhase` / `BrainStatus`,dist/core/types.d.ts)是 **6 相 + 6 字段**,server 两腿的
@@ -33,6 +34,12 @@ export type RetryStatus =
33
34
  maxRetries?: number;
34
35
  /** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
35
36
  errClass?: BrainRetryErrClass | (string & {});
37
+ /** 见 {@link BrainStatusPayload.retryAtMs}。🔴 **产生者铸的墙钟截止点**;在场时端应当拿它
38
+ * 渲倒计时,而不是拿 {@link deadline} —— 后者是**本层**按 `nowMs + 剩余量` 现算的,跨进程跳
39
+ * 的传输耗时已经把它推后了(core 顶注点名的正是这个病)。缺席 ⇒ 退回 `deadline`。 */
40
+ retryAtMs?: number;
41
+ /** 见 {@link BrainStatusPayload.errorStatus}(引擎给了才在场;缺席禁渲成 0/未知码)。 */
42
+ errorStatus?: number;
36
43
  } | {
37
44
  kind: 'error';
38
45
  deadline: number;
@@ -40,6 +47,11 @@ export type RetryStatus =
40
47
  maxRetries?: number;
41
48
  /** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
42
49
  errClass?: BrainRetryErrClass | (string & {});
50
+ /** 见 {@link BrainStatusPayload.retryAtMs}(与 `stalled` 臂同义同纪律:在场优先于 `deadline`)。 */
51
+ retryAtMs?: number;
52
+ /** 见 {@link BrainStatusPayload.errorStatus}。CC parity:`system/api_retry.error_status`
53
+ * 就是这个数,端可据它渲「API Error 529 · Retrying」这类**点名失败方**的行。 */
54
+ errorStatus?: number;
43
55
  /**
44
56
  * 🔴 **终态位**(2026-08-08 对抗复审命中):`true` ⇔ 引擎**不会再重试了**(`gave_up` 相)。
45
57
  * 缺席 = 仍在重试循环里(retrying / rate_limited / circuit_open)。
@@ -86,7 +98,8 @@ export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
86
98
  * 判据锚在「真正决定结果的量」上:决定结果的是有没有分支,不是有没有一张表。
87
99
  */
88
100
  export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'rate_limit' | 'server' | 'http' | 'output_cap';
89
- /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 7)。 */
101
+ /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 **9**——
102
+ * 真源 = server 7.54.0 `dist/trace/project.js` 的 `brainStatusEventData`,逐条条件拷贝)。 */
90
103
  export interface BrainStatusPayload {
91
104
  /** 闭集 + `(string & {})`:未知相仍可携带(开集读),不必先改类型再解析。 */
92
105
  phase: BrainStatusPhase | (string & {});
@@ -100,6 +113,40 @@ export interface BrainStatusPayload {
100
113
  attempt?: number;
101
114
  /** 引擎这一轮的重试上限。与 `attempt` 一起才能渲「2/5」。 */
102
115
  maxRetries?: number;
116
+ /**
117
+ * core 7.0.x(#506 ㋑;server ≥7.53 `brainStatusEventData` 真发;ADDITIVE,0.48.0 补)——
118
+ * 本次退避**预计结束的墙钟时刻**(epoch ms)= 发帧那一刻的 `Date.now() + retryInMs`,
119
+ * **由产生者铸**。不变式(core 顶注逐字):`retryInMs` 在场时它必在场,不宣告等待的帧
120
+ * (`recovered` / `gave_up` / output-cap 立即重发)上必缺席。
121
+ *
122
+ * 🔴 **为什么这一位必须由上游给、消费端不许自己算**(core 顶注的原话,也是本包接它的理由):
123
+ * `Date.now() + retryInMs` 只对**瞬时收到帧**的读者成立。中间隔了若干进程跳(core → server →
124
+ * 本包 → 端)之后再自己加,得到的截止点已经被传输耗时推后了;而 core 对 >30s 的等待会每 30s
125
+ * **重播一帧并递减**,于是「自己算」的倒计时在每个重播片上**重新起跳**而不是收敛。
126
+ * 🔴 **时钟域,明说以消除歧义**:墙钟(`Date.now()`),**不是**单调钟。端不得拿它与自己的
127
+ * 单调计时器比;跨机器/跨授时校正时它是**近似值** —— 权威的**相对**量始终是 `retryInMs`,
128
+ * 本位是由它派生的绝对便利位。
129
+ */
130
+ retryAtMs?: number;
131
+ /**
132
+ * core 7.0.x(#506 ㋑;server ≥7.53 `brainStatusEventData` 真发;ADDITIVE,0.48.0 补)——
133
+ * **刚刚失败的那次尝试**的 HTTP 状态码。这条通道上**唯一**一个 provider 自报的数字
134
+ * (其余一切仍走引擎自己的中性分桶 `phase` / `errClass`)。
135
+ *
136
+ * 🔴 **为什么这条一贯拒绝 HTTP 细节的通道要收它**(core 顶注的原话):要让操作者判断
137
+ * 「该等,还是该去修点什么」,两个中性桶**供不出**这个信息 —— `rate_limit` 同时盖住
138
+ * 「provider 自己会清掉的 429」与「账号配额耗尽的 429」,`server` 同时盖住 500 与
139
+ * 负载均衡器后面的 503。CC 在**同一场合**报同一个数(`system/api_retry.error_status`,
140
+ * 读自 `APIError.status`)⇒ 接它同时满足 CC parity 与本通道的中立契约。
141
+ * 🔴 **在场面是封闭的,缺席不许反推**:只在「这次尝试拿到了一个**点名了失败**的应答」时在场
142
+ * (连接梯的 retry 等待 + output-cap 的 400)。**恒缺席**于:传输层失败(压根没有应答)、
143
+ * 流中断(那条应答自己的状态是**成功**,一个什么都没失败的状态不许被当成重试的原因)、
144
+ * `circuit_open`(本地快失败,没发出去)、`recovered`/`gave_up`(不宣告任何尝试)。
145
+ * ⇒ 端**禁**把缺席渲成 0 或「未知错误码」;那两类的因由由 `errClass` 按构造点名。
146
+ * 🔴 **与 CC 的可空必填不同**:CC 有一整条 `system/api_retry` 消息专供重试场合,能把键设成
147
+ * 必填可空;本形是**所有 phase 共用的一个形**,所以是可选位(core 顶注同款理由)。
148
+ */
149
+ errorStatus?: number;
103
150
  /**
104
151
  * core 5.43.0(#307 双扫 S44,2026-08-19;ADDITIVE)——**这次等待的原因分桶**
105
152
  * (core `BrainRetryErrClass`,provider 中立闭集)。与 `phase`(引擎正在**做什么**)互补:
@@ -120,7 +167,7 @@ export interface BrainStatusPayload {
120
167
  * engine-vocab 门 G2-c 拿它与 core `BrainStatus` 的键集逐元素比 ⇒ 引擎 additive 增键当天红。
121
168
  * 下面两个类型钉保证镜像与 interface 之间不可能漂移(少键/多键都是编译错)。
122
169
  */
123
- export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass"];
170
+ export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass", "retryAtMs", "errorStatus"];
124
171
  /**
125
172
  * BrainStatus 载荷 → spinner 行状态。
126
173
  *