@sema-agent/client-core 0.78.0 → 0.78.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -49,6 +49,30 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.78.1(2026-09-21)
53
+
54
+ > 主题:**两辆并行车收货 + 三张跟进票**(patch;公面运行期导出 1143 → **1154**;wire 零新键;peer 地板不动 `>=11.0.1`;一处既有导出的行为收紧 + 三处措辞改口)—— 工具结果正文人类面读口 `displayBody` 与围栏解析配对收紧(CC-113)· MCP 面板 / 活性两处「没报」措辞分句(CC-115)· 记忆治理面五动词调用口(CC-97 b)· 非流式提交回执的起手接线回执读口 `readSubmitWiringManifest`(CC-121)· 三份失败判官的抛出物读取收口成一只永不抛的快照 helper(CC-119)。**成文改口段见 §83 83y。**
55
+
56
+ ### Added
57
+
58
+ - **`displayBody(text)`**(CC-113):引擎交给模型的文本外面那层不可信围栏由本包铸、本包读,剥壳也归本包 —— `{ fenced: true, label, body } | { fenced: false, body } | undefined`。只剥本包认识的形、只剥最外层;认不出 / 半截 / 头尾标签不同一律 `fenced: false` 且 `body` 逐字 = 入参;**非串答 `undefined`**(本端手上没有可读文本;与空串 `{ fenced: false, body: '' }`、围栏里的空正文 `{ fenced: true, label, body: '' }` 三形互不折叠);不消毒、不封长(呈前清洗归渲染侧)。类型 `DisplayBody` 同批导出。
59
+ - **记忆治理面五动词调用口**(CC-97 b;sdk ≥11.0.1 型面):`readEntryProvenance` / `eraseMemoryEntries` / `listExternalOriginEntries` / `listOriginClearances` / `clearEntryOrigin`(注入缝 `MemoryFacade`,入参 / 回体型全取自 sdk)+ 单一失败判官 `classifyMemoryFailure`(🔴 出处先于状态码:无机读码的 4xx / 501 / 405 一律 `unknown / no_verdict`;分辨位在 `errorCode` 不在回体也不在 status)+ 两张闭集词表 `MEMORY_VERB_REFUSAL_CAUSES`(21 词,「确知什么都没做」)/ `MEMORY_VERB_UNKNOWN_WHYS`(9 词,「不知道做没做成」)+ `externalOriginCoverage`(空答绝不读作本店干净;联合里没有「干净」臂)。能力位 = 明确的否才拦(`refused / face_absent`,零请求),「说不出」照发;两条写腿的 200 体复用 0.76.1 的读口;非幂等动词零重试;三口带身份对账(`identity_mismatch`)。
60
+ - **`readSubmitWiringManifest(result)`**(CC-121):非流式 `POST /v1/tasks` 200 体 `TaskResult.wiringManifest`(server ≥7.92.0 additive)的读口,三态 `not_reported`(键自有缺席 = 老 server / 没产该帧的部署形)/ `unreadable` / `manifest{view}`;🔴 `view` 与流式腿 chrome 臂**同一张**(去掉 `kind` / `laneProof`;两条腿共用同一只拼装口 + 单源窄化器,门以同一份对象两腿逐字节对拍);只读工具面不读活性(`mcp[].liveness` 原样过境);operator 形多出的段只透传;零工具面 `tools.count === 0` 是真读数。类型 `WiringManifestView` / `WiringManifestSections` / `SubmitWiringManifestReading` 同批导出。
61
+
62
+ ### Changed(行为 / 措辞;详见 §83 83y)
63
+
64
+ - **`stripUntrustedFence` 围栏解析收紧**(CC-113):结束标签必须与开始标签**逐字相等**才算一只完整围栏。此前结束标签是任意文本,两类文本被误判成完整围栏:开始与结束标签不同的;开始标签在、结束标签被截断而正文里恰有一只完整内层围栏的 —— 后者会把内层那一行结束标记**静默删掉**。收紧依据是上游铸点单点、开闭标记插的是同一个标签值 ⇒ 真引擎产出零影响;把这只口用在非引擎产出文本上的端按 §83 复核。
65
+ - **MCP 面板「最近一条腿」整键缺席那一句**(CC-115):从「保留窗内没有腿 / 最近一条腿没有名册 / 引擎早于这个字段」改成 `last leg mcp not reported (the panel carried no entry for the last leg; this client cannot tell which cause applies)` —— 旧句摆了一份假闭集(上游列的缺席形有七项,含「这条腿有属主而调用方不是属主」)并做了契约明禁的版本反推。
66
+ - **MCP 活性两句**(CC-115):`indeterminate` ⇒ `… (no liveness observation is available to this client, and this client cannot tell which cause applies)`(四源同形,本端连名册都未必看过,不再替名册作证);入参缺席 ⇒ `… (this client holds no liveness reading for this leg)`(与前者拉开逐字距离)。
67
+ - **三份失败判官的抛出物读取收口**(CC-119):持久规则店 / 会话策略店 / 记忆治理面此前各自一份 `errShape`,前两份裸读属性并调用无保护的 `String(e)` —— 注入的 facade 拒绝一个带抛出 getter 的对象时,判官在 catch 块里再抛,整只口以 reject 结束,调用方拿不到原定的 `unknown` / indeterminate 结局(写没写根本判不出)。三份收口成一只永不抛的 `wireFailureShapeOf`(每格恰读一次并快照;读不了 / 渲不出各一句兜底;`retryAfterMs` 同样保护)。可见差别只对注入非常规错误对象的宿主存在:此前 reject,现在按「无码 ⇒ 读不出出处」落 `unknown` 臂。同根同批:出处账读口 `readEntryProvenance` 的**嵌套**判别位(`binding.state` / `custody.state` / `custody.events` / `exclusion.code`)此前在保护外读取,带陷阱的账让整只口 reject ⇒ 各读一次并快照;自有 `exclusion` 键在场但 `undefined` / `null` 不再借 `!== undefined` 豁免溜成「没有扣留」⇒ `unknown / result_unreadable`。
68
+ - 流式帧 `wiring_manifest` chrome 臂改经共用拼装口 `wiringManifestViewOf`(与非流式读口同源);产出逐字节同 0.78.0。
69
+
70
+ ### Gates
71
+
72
+ - 新门三只:`run-display-body-test.mjs`(82:单源对拍二十一形 / 只剥最外层 / 头尾配对 + 上游铸点真字节直证 / 非串诚实缺席三形互不折叠 / 不越界洗 / 源码级单源)· `run-memory-verbs-wire-test.mjs`(216:嵌套 getter / revoked Proxy 五形不 reject、扣留位 undefined / null 两形拒认 / 逐动词能力位 / 成功臂 / 逐码一格 / 无码 ⇒ no_verdict / 200 坏体不认成功 / 假件注入 / 账目可达性 / 上游字节锚)· `run-submit-wiring-manifest-test.mjs`(33:三态互不折叠 / 两腿同一张视图逐字节对拍 / operator 形透传 / 零工具面真读数 / eventId / 型面含两道编译期等值钉 —— 双向可赋值 + 键集相等,只钉可赋值时多加一个可选段仍编译得过)。
73
+ - 既有门扩格:`run-mcp-panel-projection-test.mjs` W 段(逐因可达 / 句集互异 / 分辨不了只在那一句 / 不咎版本 / 不摆假闭集 / sdk `McpStatusPanel` 成员签名 ⇄ 已审阅基线漂移钉)+ G4 成文改口 · `run-mcp-liveness-test.mjs` W 段 9 格 · `run-persisted-rule-write-test.mjs` W12(抛出物访问器 / toString / retryAfterMs 各抛 ⇒ 不 reject)· `run-rules-side-test.mjs` G3z · `run-session-policy-wire-test.mjs` R6b / R6c。
74
+ - 登记物:公面 1143 → 1154;typeshape `unknown` 出境 390 → 393(`displayBody(text: unknown)` / `classifyMemoryFailure(e: unknown)` / `wireFailureShapeOf(e: unknown)`,三处都是边界读口入参)+ 负控锚同批;index 闭包 191 → 194(`memoryVerbsWire` / `wireFailureShape` / `wiringManifestView`);singleton 清单 +2;`export-liveness.json` 销掉 `stripUntrustedFence` 那行欠账(它欠的那只包侧门就是 `run-display-body-test.mjs`)。
75
+
52
76
  ## 0.78.0(2026-09-21)
53
77
 
54
78
  > 主题:**peer sdk 地板抬到 11.x + 单步写口读回按活性判别 + 第十词专句 + 整只工具面卸载进车道表**(🔴 **minor**:peer 地板 `>=9.8.1` → **`>=11.0.1`**;单步写口结局联合**多一臂 `revoked`**、`RulesFacade.write` 转必填、三只自铸型改为 sdk 别名(型面 BREAKING 三处,编译器会报);公面运行期导出 **1143 不变**;wire 新键一枚 `excludeAllTools`(sdk 具名位进车道表))—— 抬地板(CC-106)· 单步写口 200 体按 `stillLive` 判别联合改读(CC-103)· `DeniedBy` 第十词 `read_boundary` 专句(CC-106 ④)· `excludeAllTools` 车道行。**成文改口段见 §82 82y,按表态制点名三端。**
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.78.0
38
+ **Version:** 0.78.1
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -333,6 +333,7 @@ public-surface guard checks that last one).
333
333
  | `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once |
334
334
  | `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation, the discussion workflow name). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports |
335
335
  | `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
336
+ | `scripts/run-display-body-test.mjs` | The engine wraps text it hands a model in a fence — an opening marker naming the payload, the payload itself, and a closing marker — so the model reads it as data and not as instructions. That fence is minted and read in one place here, which makes stripping it for a human reader this package's job rather than each shell's: a shell that renders the envelope verbatim is showing a person a defence that was written for a model. The reader answers with a discriminated union — fenced, with the label and the payload, or not fenced, with the text as it came in — and it reaches that answer through the **same** matcher the mint side registers, never a second copy of it; the guard proves that by walking the syntax tree of every source file and requiring exactly one literal carrying the marker text, and by requiring the reader's own body to contain no matcher of its own. Eighteen shapes are run through both entry points and required to agree line for line. Anything the package does not recognise — a near-miss in the wording, a hyphen where the marker has a dash, a different case, an opening marker with no close, a close before an open, a truncated close, or any non-whitespace byte outside the pair — comes back unfenced with the input returned **verbatim**: no guessing, no trimming, no repair, because a half-stripped envelope puts a sentence on screen that nobody wrote. Only the outermost layer is removed, so a nested fence, or one forged inside the payload, survives byte-for-byte in the body — those bytes are part of what the engine said, not part of this protocol. Nothing else is washed: control characters, leading and trailing whitespace and a twenty-thousand-character payload all pass through untouched, and so does the label, because sanitising and length-capping belong to the mint point that puts a string on a screen and a passage of text must not have two launderers. A value that is not text is answered with **nothing at all** rather than with an empty payload: the reader never stringifies it, never calls its `toString`, and never emits `[object Object]`, and it does not hand back a body of zero length either — an empty payload is a real reading (a fence can legitimately wrap nothing, and an empty string is an empty string), so folding "there was no readable text" into it would leave a caller unable to show a degraded line at all. Those three stay apart: no text yields nothing, an empty string yields an unfenced empty payload, and an empty fenced payload yields a fenced one with its label. The `fenced` discriminator is always present on a reading, and the label key exists only on the fenced arm, so a missing label is never rendered as an empty one |
336
337
  | `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
337
338
  | `scripts/run-seat-task-request-origin-test.mjs` | Where every field of the seat lane's send-message payload comes from, and whether it actually lands anywhere. The seat payload is a closed interface this package mints itself, and most of its fields are meant to ride verbatim onto the engine's request body — two facts nothing used to connect, so both directions could drift in silence. A seat field could be named after a request position that does not exist, in which case a client writes to it, the wire carries it, the engine ignores the whole key, and the screen shows a switch that does nothing; conversely a new request position could arrive with no seat to sit in, which is **structural** absence — the closed set *is* the carrier, so a decision missing from it has nowhere to be put at all, the same shape logged when the effort dial had no seat. The guard turns each field's origin into data: either it names the request position it forwards to, or it is declared seat-local with a written reason, and the two are mutually exclusive. Forwarding claims are then checked against the **installed** SDK's type declarations, parsed rather than restated — a hand-copied list of position names would only ever prove that two transcriptions agree. The parser is held to reading top-level positions only, since a nested option object's inner keys would otherwise be mistaken for positions of the request itself, and it proves that discrimination on synthetic input before any verdict is given. The two subagent fields carry a standing regression pin, and the retention window's inner keys are read from the declaration the same way, so a seat that offers a tunable window cannot offer one the wire has no room for |
338
339
  | `scripts/run-wire-auth-source-test.mjs` | **When** the outbound credential is read. A literal string is consumed at construction — the transport captures it in a closure and every later request reuses that one copy — so once the engine is replaced by another session and the credential rotates, a long-lived client keeps presenting the old one and the only way out is to rebuild the client along with everything hanging off it. The credential position now also accepts a getter that is called **once per outbound request**. The guard anchors on the deciding quantity, which is not "was the getter called" — reading once at construction and reusing the result would satisfy that too, and is exactly the shape being removed — but *which read produced the value on the wire*: it changes the getter's answer between two requests through the same client and requires the second request to carry the new one, and it requires construction to read the getter **zero** times. The three-state credential semantics are replayed per request rather than assumed: on loopback an unavailable credential sends **no** authorization header at all rather than a fabricated one, off loopback it sends the fail-closed anonymous identity so the deployment answers with an honest 401, and the guard shows a single client moving between those states across successive requests. A getter that throws is fail-soft — the request still goes out under the no-credential branch, because a broken credential port should not take the whole wire down, and the exception may itself carry credential material. The same-origin relay form is checked to stay out of the getter path entirely, and every request is checked to keep the credential in the authorization header only — never in the URL, never in another header |
@@ -373,6 +374,7 @@ public-surface guard checks that last one).
373
374
  | `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 |
374
375
  | `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" |
375
376
  | `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 0.71.0 adds sections F–I: the fourth/fifth/sixth manifest sections (`tools` via the roster reader, `hooks[]` rows dropped one by one when malformed, `lsp` absent unless `mounted` is a boolean), the `tool_roster_delta` arm (narrowed `delta`, `malformed` when `fromDigest`/`roster` cannot be read, host applies it against its own digest), the `context_usage` arm (finite-gated scalars plus `sections[]` rows dropped one by one), and the `WiringManifestMcpEntryView` rename with `MAX_AGENT_SKILLS` gone from the surface |
377
+ | `scripts/run-submit-wiring-manifest-test.mjs` | The non-streaming submit receipt can carry the run's opening wiring manifest (`TaskResult.wiringManifest`, additive on newer servers). `readSubmitWiringManifest` answers one of three: the key is absent on the receipt itself (older server, or a deployment whose engine never produced that frame) — not the same as unreadable; the key is present but cannot be read (not an object, or none of the nine sections survive); or a manifest view. The view is the same shape the streaming lane's chrome event carries (minus its two envelope keys) and is assembled by the same code path, so both lanes agree byte for byte on the same object. Liveness fields ride through untouched — this reader never mints a liveness verdict — and an operator-shaped receipt with extra governance sections reads to the same view as a tenant-shaped one. A zero-tool roster is a real reading, not an absence. |
376
378
  | `scripts/run-rule-offers-reader-test.mjs` | The narrowing reader behind the "don't ask again" options, now a public entry point rather than a card-port-only one. Hosts that render the frame themselves (a browser has no three-way terminal card) previously had to rebuild this reader on their side, and what it carries is a **redemption-safety** judgement, not a convenience: the batch arm is redeemed by **index**, so a reader that compacts the array after dropping a malformed entry makes the k-th option a person clicked and the k-th rule the server writes two different rules. So: a bad entry is dropped **on its own** (one bad option must not make a real one disappear) while every surviving entry keeps its **original wire index** — pinned from both ends, with the bad entries leading and trailing. A batch's *members* are the opposite: any malformed member drops the whole batch, because a conjunctive batch is one "yes" to all of them and a batch missing a member is a different grant; its honest-remainder count is a reading, not decoration, so a non-integer or negative value drops the batch rather than rendering a fabricated zero. An empty array, a non-array, an over-cap array and an all-bad array all read as **absence** rather than an empty list, because an empty list renders as "there is an option lane with nothing in it". The two wire generations are ordered by a rule, not a preference: the newer key wins outright, a newer key that is **present but unreadable** does not fall back to the retired key (borrowing the older material would pass someone else's options off as this request's), and a `null` newer key reads as absence so a relaying layer that serialises "missing" as null cannot delete the whole lane on older engines. The public entry is finally reconciled against **both** card-port legs on the same material, byte for byte, so the exported reader and the one the card sees can never become two. Two upstream vocabularies used to be **hand-copied** here, and both had fallen behind: a match word outside the copied pair dropped an otherwise valid option outright, and a batch carrying a directory-read member — a member kind the copy did not know — dropped the whole batch. Both tables now come from one place upstream and are re-exported verbatim, pinned in both directions: every word in the table must be accepted (a narrower copy reds on the words it never learned) and a word constructed to be outside it must still be refused (a reader widened to "any string" reds too), with the retired-key normalising leg sharing the same narrowing so the fix cannot land on one leg only. A member whose kind is genuinely unknown still drops **the whole batch and only that batch** — never one member, because a conjunctive batch one member short renders "yes to N" as "yes to N−1", and never the card, because the honest single beside it is intact — while a member from before the discriminant existed normalises to the historical kind rather than being refused. The additive per-segment reasons ride through verbatim, drop only the row that is malformed, and stay **absent rather than empty** when nothing survives, since an empty list would read as "confirmed nothing uncovered" while the count remains the only source of truth |
377
379
  | `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 |
378
380
  | `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 |
@@ -398,6 +400,7 @@ public-surface guard checks that last one).
398
400
  | `scripts/run-peer-lane-rules-write-capability-test.mjs` | Two more engine self-descriptions read the same four-state way as their seven sibling capability readers (`capabilities.peerLane`, `capabilities.permissionRulesWrite`): an absent key is not reported (an older engine that predates the position, never folded into `false`), `true` is present, `false` is a positive absent (the cross-session lane not being mounted on this deployment, or this particular call not being able to reach the tightening-direction write entry), and anything else is unreadable and drops the cell. Each carries its own single-source verdict (`peerLaneAvailable` returns `yes`/`no`/`unknown`; `permissionRulesWriteAvailable` collapses to a plain boolean, present being the only `true`). The write-entry position pairs with a boolean convenience port in the persisted-rules module, and this guard pins that port to derive from nothing but this one reader's own reading — never a conjunction with the lane-reachable position, and never a second read of the deployment-level existence signal the revoke surface uses (the two are documented as reading differently on purpose): a deployment where the lane answers true but the write entry's key is simply absent (an older binary) must still come back `false`, a deployment where the write entry answers true while the lane key is entirely unseen must still come back `true` (proving no silent conjunction crept in), seeding only the general capabilities cache — never this reader's own feed — must still come back `false` (proving the convenience port cannot be satisfied by the wrong table), and passing an explicit `undefined` base URL must still come back `false` even while a different, already-installed engine target answers `true` for the same position (an adversarial pass found the naive forward of that parameter falls through to the reader's own convenience default, silently answering for whichever engine happens to be installed rather than the caller's absent target — the fix routes an explicit absence through the same empty-string path the reader treats as unobserved). |
399
401
  | `scripts/run-lane-proof-identity-test.mjs` | The **instance identity of a lane proof**: the main-lane proof is minted fresh on every emission. Previously a single module-level constant object was handed both to `laneOf(an unregistered task id)` and to some fifty main-lane emission points, so two unrelated consumers — across adapter instances, across streams, across turns — held the same object: writing a card id onto one of them was readable on the other, and the four opening main-lane events changed together. Nothing in this package writes to a lane proof and the known consumers only read it, so this is an **aliasing hazard on a published output surface** rather than an observed corruption — a consumer that uses the proof as an identity key, for dedup, or as a view-layer identity would conflate two unrelated rows without writing a single byte, which is precisely the half that freezing the object would not solve. The gate therefore anchors on instance identity: two independent adapter instances, two rows inside one instance, the same id read twice, and two arms in one beat are each distinct references; mutating one leaves the others byte-identical; and the subagent lane, which already minted fresh, is the control that proves the criterion discriminates. The main-lane **value** is unchanged — an unregistered id still answers `{lane:"main"}` with exactly one own key and still emits its events, so absence is not turned into a second kind of absence — with ordering pinned three ways (registered-then-read, read-then-registered with no retroactive edit of an already delivered proof, the same id twice) and the id failure classes pinned four ways (unregistered, empty string, absent, non-string, the last two emitting no panel event at all rather than an ownerless proof). Where one row emits **two** events — the terminal-tick and card-close legs, which each yield a lifecycle stop and a panel end — the attribution is decided once (a consumer binding a card between the two yields must not split one row across two lanes) while each event still gets its own proof, so a host consuming them one at a time cannot poison the second before it is even yielded. The run stream leg is covered as the same shape, and a syntax-tree check forbids reintroducing a module-level lane-proof object literal or a module-level `LaneProof`-annotated binding (judged on the type node, not on text, so a compile-time pin tuple that merely mentions the type is not miscaught), backed by a type-checker pass that also catches an un-annotated module-level cache such as `const x = mainLane()` while letting the callable factory itself through, while the module-private three-state sentinels of the untrusted read are frozen instead — only `Object.freeze` counts, never `Object.seal`, which still permits writes to existing keys — their exposure being confined to one module |
400
402
  | `scripts/run-memory-entries-wire-test.mjs` | The two memory-governance capability bits and the three memory-entry response readers. Each bit (`capabilities.memoryCompliance`, for the entry-provenance and erasure endpoints; `capabilities.memoryOrigin`, for the external-origin listing and clearance endpoints) is read the same four-state way as its sibling capability readers: an absent key is reported as not reported (never folded into `false` — an older engine simply does not answer, and the right next step is to try the endpoint and read its 501), `true` is the face being mounted, `false` is a positive "not on this deployment" (the wire does not distinguish a backend without control-plane ownership from an empty operator roster, so the wording never guesses which), any non-boolean value is unreadable and drops the cell instead of being folded into "absent", and a capabilities body that is not an object at all is unreadable rather than "not reported". The two bits deliberately stay **two** readers with two separate per-engine tables, because the engine deliberately keeps them two separate product faces even while they happen to carry the same value today: feeding one an unreadable body, or invalidating one, leaves the other's reading untouched, and a body where one is on and the other off is answered one bit at a time. The entry-export reader narrows each row on its own (an empty array really is zero rows, a non-empty array with nothing readable in it is reported as unreadable rather than as "no rows", and partly bad rows are kept with a dropped count), reads the external-origin marker as three states rather than a boolean (the two structural carriers mark a row; a row whose frontmatter cannot be read, or which carries the third, suspended-form carrier, is undecidable, because the judge for that carrier lives in the engine and this package refuses to mint a second copy of it), and treats an unreadable "is this the whole scope" flag as "not the whole scope". Its verdict port implements — in code, not in a comment — the rule that an empty answer is never a clean store: the caller must state whether the request declared origin-awareness, because this endpoint withholds marked entries by default and the two bodies are shaped identically, so without that statement an empty answer is only ever "unknown"; the affirmative answer is scoped to the one named scope and carries that scope with it, and the type has no store-wide arm at all. The erasure receipt reader keeps three things apart that are easy to collapse: "this call erased nothing" (a real receipt whose erased list is empty and whose not-found list explains why, per id), "a 200 with an empty body", and "a body that could not be read" — at the reading, the counting and the verdict layer alike; it refuses a version envelope it does not recognise instead of reinterpreting it, treats the three closed vocabularies as closed (an unknown word is unreadable, never folded into a known arm), keeps an unreadable binding as unknown instead of claiming "unbound", passes the "history cannot be judged" flag through as four states (set, explicitly unset, absent, and present-but-unreadable — an unreadable flag is kept distinct from an absent one, and the history verdict then answers "unknown" rather than the stronger claim), and answers the replay question as three states so that the degraded lane is never retried automatically. The clearance receipt reader carries the cleared marker through verbatim and says separately whether it was reported at all. Every array in every response is snapshotted once — the length is read exactly once and each index exactly once, rather than iterating the caller's own iterator — because an array that reports one length while being walked and another afterwards could otherwise have a marked row quietly dropped while the "was anything unreadable" check saw nothing, which ends in calling the scope clean; an array that reports an absurd length is reported as unreadable rather than silently truncated to its first rows. All three readers never throw. |
403
+ | `scripts/run-memory-verbs-wire-test.mjs` | The five memory-governance verbs as call ports — entry provenance, compliance erasure, the external-origin listing, the clearance ledger and the un-mark valve — on top of the readers above. One failure judge serves all five, and its first question is **provenance, not status**: the engine stamps a machine code on every refusal it mints, so a 501, 405, 409, 404 or 400 that carries **no code** proves nothing about who answered — a proxy or gateway returning the same status may well have passed the request on first — and every such answer is reported as "no verdict" rather than as "nothing happened". Twenty-one coded refusals each get their own arm, branched on the code alone: the status cannot tell them apart (nine different operator actions ride the same 409 here), and conjoining the status would silently demote a refusal the day the engine moved it. A coded 5xx, a coded answer with no status at all, and a coded 4xx this version does not recognise all land in the "cannot tell" arm, because on a non-idempotent verb the default for "could not classify" must be "do not know", never "did not happen". The judge is called from catch blocks, so each of its own property reads is guarded: an error object whose accessors throw is classified, not re-thrown. The capability gate runs before the call and reads the two bits the engine keeps deliberately separate (one for provenance and erasure, one for the origin faces); only an engine that positively says the face is off stops the request, while "this binary does not report that bit" and "this process never saw a capabilities body" both still send — folding "cannot say" into "is not there" is the dishonest-absence shape this package refuses, and these routes answer the capability gate before touching anything. A missing capability reading is a named, explainable error rather than a silent default that would answer for whichever engine happens to be installed. Each verb returns its own discriminated union whose success arm, refusal arm and cannot-tell arm share no keys, so a consumer cannot express "could not read it" as "it worked". The two write legs reuse the erasure and clearance receipt readers rather than minting a second copy, which keeps "this call erased nothing", "a 200 with an empty body" and "a body that could not be read" three separate things here too; the provenance account is narrowed only to its envelope and discriminants and otherwise passes through verbatim, and an account stamped with a newer envelope version is refused rather than reinterpreted. All three receipt-bearing verbs additionally reconcile identity — the account id, the attestation request id and the clearance receipt entry id must be the ones that were sent — because a readable receipt is not yet a receipt about this call. The origin listing carries the server's own echo of the scopes it actually audited, the type has no store-wide arm, and the coverage port takes a mandatory second argument and has no "clean" arm at all: the strongest thing it will say is which scopes were audited. Neither write leg is ever retried, including the refusal whose documented recovery is to send again, because that resend completes whichever clearance row is already open and the audit attribution on it is a person's signature. Request bodies are handed over verbatim — the degraded-erasure authorization is never injected — while the scope list is sent as the snapshot this port validated, so an array that reports one length while being read and another afterwards cannot make "the list I checked" and "the list I sent" two different things. |
401
404
  | `scripts/run-dist-orphan-test.mjs` | Every `.js` / `.d.ts` under `dist/` must have a same-named source under `src/`, and every source must have its build output — because the compiler only writes and never deletes, so a module removed from the sources keeps shipping from the previous build (the whole `dist/` directory is on the publish whitelist) while the public-surface gate only looks at what the barrel exports and the hygiene gate only looks at forbidden words. Orphans are named one by one; the pre-publish posture is a clean rebuild, and this gate is the check that the posture was actually followed. |
402
405
  | `scripts/run-dist-comments-test.mjs` | **dist ships zero comments.** Since 0.77.2 the build strips comments (`removeComments`); this gate walks every shipped `dist/**/*.js` / `*.d.ts` and counts comment trivia with the TypeScript scanner (string literals containing `//` and generator methods are not comments), failing on the first one (`DIST-COMMENT-FAIL`). Source comments are an internal surface; what still ships is code, string literals and type-level text, which the hygiene gate screens. Negative control: one plain comment appended to `dist/index.js` turns it red. |
403
406
  | `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. |
@@ -12,6 +12,7 @@ import { isTerminalStatus } from '../runTerminal.js';
12
12
  import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
13
13
  import { fleetRowAgentType } from '../fleet/fleetRowAgentType.js';
14
14
  import { chrome, mainLane, transcript, messageIdentityOf } from './ids.js';
15
+ import { wiringManifestViewOf } from '../adapter/downstream/wiringManifestView.js';
15
16
  import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
16
17
  const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
17
18
  const message = m.message;
@@ -267,40 +268,10 @@ const engineNoticeArm = function* (m) {
267
268
  const wiringManifestArm = function* (m) {
268
269
  if (isSubFlowSegmentEnd(m))
269
270
  return;
270
- const modelGate = m._sema_modelGate;
271
- const autoMode = m._sema_autoMode;
272
- const mcp = m._sema_mcp;
273
- const tools = m._sema_tools;
274
- const hooks = m._sema_hooks;
275
- const lsp = m._sema_lsp;
276
- const writeProtection = m._sema_writeProtection;
277
- const hasWriteProtection = typeof writeProtection === 'object' && writeProtection !== null;
278
- const autoConsolidation = m._sema_autoConsolidation;
279
- const hasAutoConsolidation = typeof autoConsolidation === 'object' && autoConsolidation !== null;
280
- const readDeny = m._sema_readDeny;
281
- const hasReadDeny = typeof readDeny === 'object' && readDeny !== null;
282
- const hasTools = typeof tools === 'object' && tools !== null;
283
- const hasHooks = Array.isArray(hooks);
284
- const hasLsp = typeof lsp === 'object' && lsp !== null;
285
- const hasGate = typeof modelGate === 'object' && modelGate !== null;
286
- const hasAuto = typeof autoMode === 'object' && autoMode !== null;
287
- const hasMcp = Array.isArray(mcp);
288
- if (!hasGate && !hasAuto && !hasMcp && !hasTools && !hasHooks && !hasLsp && !hasWriteProtection && !hasAutoConsolidation && !hasReadDeny)
271
+ const view = wiringManifestViewOf(m);
272
+ if (view === undefined)
289
273
  return;
290
- yield chrome({
291
- kind: 'wiring_manifest',
292
- laneProof: mainLane(),
293
- ...(hasGate ? { modelGate: modelGate } : {}),
294
- ...(hasAuto ? { autoMode: autoMode } : {}),
295
- ...(hasMcp ? { mcp: mcp } : {}),
296
- ...(hasTools ? { tools: tools } : {}),
297
- ...(hasHooks ? { hooks: hooks } : {}),
298
- ...(hasLsp ? { lsp: lsp } : {}),
299
- ...(hasWriteProtection ? { writeProtection: writeProtection } : {}),
300
- ...(hasAutoConsolidation ? { autoConsolidation: autoConsolidation } : {}),
301
- ...(hasReadDeny ? { readDeny: readDeny } : {}),
302
- ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
303
- });
274
+ yield chrome({ kind: 'wiring_manifest', laneProof: mainLane(), ...view });
304
275
  };
305
276
  const approvalFrameArm = (kind) => function* (m) {
306
277
  const schemaVersion = m.schemaVersion;
@@ -1,4 +1,5 @@
1
- import type { AgentEvent, CheckpointGate } from '@sema-agent/sdk';
1
+ import type { AgentEvent, CheckpointGate, TaskResult } from '@sema-agent/sdk';
2
+ import { type WiringManifestView } from './wiringManifestView.js';
2
3
  import { type SDKMessage, type EmitContext, type ModelUsage } from '../types.js';
3
4
  import { type McpLivenessView } from '../../mcpLiveness.js';
4
5
  export declare const INTERNAL_SDK_ARM_TYPES: ReadonlySet<string>;
@@ -38,6 +39,15 @@ export interface WiringManifestMcpEntryView {
38
39
  livenessUnreadable?: true;
39
40
  }
40
41
  export declare function projectMcpSection(raw: unknown): WiringManifestMcpEntryView[] | undefined;
42
+ export type SubmitWiringManifestReading = {
43
+ kind: 'manifest';
44
+ view: WiringManifestView;
45
+ } | {
46
+ kind: 'not_reported';
47
+ } | {
48
+ kind: 'unreadable';
49
+ };
50
+ export declare function readSubmitWiringManifest(result: TaskResult): SubmitWiringManifestReading;
41
51
  export interface WiringManifestWriteProtectionView {
42
52
  targetView: 'spelling-only' | 'target';
43
53
  }
@@ -1,3 +1,4 @@
1
+ import { wiringManifestViewOf } from './wiringManifestView.js';
1
2
  import { stamp, snapshotSegmentIdentity, } from '../types.js';
2
3
  import { turnUsageToModelUsage } from './turnUsageToModelUsage.js';
3
4
  import { gateOutcomeOf } from '../../gateOutcome.js';
@@ -515,6 +516,21 @@ function projectAutoModeSection(raw) {
515
516
  : undefined;
516
517
  return { armed, reason, ...(deniedSource !== undefined ? { deniedSource } : {}) };
517
518
  }
519
+ export function readSubmitWiringManifest(result) {
520
+ if (typeof result !== 'object' || result === null || Array.isArray(result))
521
+ return { kind: 'unreadable' };
522
+ if (!Object.hasOwn(result, 'wiringManifest'))
523
+ return { kind: 'not_reported' };
524
+ const raw = result.wiringManifest;
525
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
526
+ return { kind: 'unreadable' };
527
+ const body = wiringManifestSupersetBody(raw);
528
+ if (body === undefined)
529
+ return { kind: 'unreadable' };
530
+ const eventId = raw.eventId;
531
+ const view = wiringManifestViewOf({ ...body, ...(typeof eventId === 'string' && eventId.length > 0 ? { eventId } : {}) });
532
+ return view === undefined ? { kind: 'unreadable' } : { kind: 'manifest', view };
533
+ }
518
534
  function wiringManifestSupersetBody(ev) {
519
535
  const modelGate = projectModelGateSection(ev.modelGate);
520
536
  const autoMode = projectAutoModeSection(ev.autoMode);
@@ -0,0 +1,27 @@
1
+ import type { WiringManifestModelGate, WiringManifestAutoMode, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView } from './eventToSdkMessage.js';
2
+ import type { ToolRosterView } from '../../toolRoster.js';
3
+ export type WiringManifestSections = {
4
+ _sema_modelGate?: WiringManifestModelGate;
5
+ _sema_autoMode?: WiringManifestAutoMode;
6
+ _sema_mcp?: WiringManifestMcpEntryView[];
7
+ _sema_tools?: ToolRosterView;
8
+ _sema_hooks?: WiringManifestHookEntryView[];
9
+ _sema_lsp?: WiringManifestLspSeamView;
10
+ _sema_writeProtection?: WiringManifestWriteProtectionView;
11
+ _sema_autoConsolidation?: WiringManifestAutoConsolidationView;
12
+ _sema_readDeny?: WiringManifestReadDenyView;
13
+ eventId?: string;
14
+ };
15
+ export interface WiringManifestView {
16
+ modelGate?: WiringManifestModelGate;
17
+ autoMode?: WiringManifestAutoMode;
18
+ mcp?: readonly WiringManifestMcpEntryView[];
19
+ tools?: ToolRosterView;
20
+ hooks?: readonly WiringManifestHookEntryView[];
21
+ lsp?: WiringManifestLspSeamView;
22
+ writeProtection?: WiringManifestWriteProtectionView;
23
+ autoConsolidation?: WiringManifestAutoConsolidationView;
24
+ readDeny?: WiringManifestReadDenyView;
25
+ eventId?: string;
26
+ }
27
+ export declare function wiringManifestViewOf(s: WiringManifestSections): WiringManifestView | undefined;
@@ -0,0 +1,23 @@
1
+ const _viewPin = true;
2
+ void _viewPin;
3
+ const _viewKeysPin = true;
4
+ void _viewKeysPin;
5
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
6
+ export function wiringManifestViewOf(s) {
7
+ const view = {
8
+ ...(isObj(s._sema_modelGate) ? { modelGate: s._sema_modelGate } : {}),
9
+ ...(isObj(s._sema_autoMode) ? { autoMode: s._sema_autoMode } : {}),
10
+ ...(Array.isArray(s._sema_mcp) ? { mcp: s._sema_mcp } : {}),
11
+ ...(isObj(s._sema_tools) ? { tools: s._sema_tools } : {}),
12
+ ...(Array.isArray(s._sema_hooks) ? { hooks: s._sema_hooks } : {}),
13
+ ...(isObj(s._sema_lsp) ? { lsp: s._sema_lsp } : {}),
14
+ ...(isObj(s._sema_writeProtection) ? { writeProtection: s._sema_writeProtection } : {}),
15
+ ...(isObj(s._sema_autoConsolidation) ? { autoConsolidation: s._sema_autoConsolidation } : {}),
16
+ ...(isObj(s._sema_readDeny) ? { readDeny: s._sema_readDeny } : {}),
17
+ };
18
+ if (Object.keys(view).length === 0)
19
+ return undefined;
20
+ if (typeof s.eventId === 'string' && s.eventId.length > 0)
21
+ view.eventId = s.eventId;
22
+ return view;
23
+ }
@@ -1,4 +1,5 @@
1
1
  import { engineCapTrue } from '../engineCapsCache.js';
2
+ import { wireFailureShapeOf } from '../wireFailureShape.js';
2
3
  import { hostLog } from '../host.js';
3
4
  import { observedPermissionRulesWrite, permissionRulesWriteAvailable } from '../permissionRulesWriteCapability.js';
4
5
  import { readToolApprovalRespondAck } from './toolApprovalWire.js';
@@ -33,22 +34,8 @@ export function readCcImportRedeemCounts(result) {
33
34
  }
34
35
  return null;
35
36
  }
36
- function errShape(e) {
37
- const o = (e ?? {});
38
- return {
39
- ...(typeof o.status === 'number' ? { status: o.status } : {}),
40
- ...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
41
- message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
42
- };
43
- }
44
- function retryAfterSecOf(e) {
45
- const ms = e.retryAfterMs;
46
- if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
47
- return undefined;
48
- return Math.ceil(ms / 1000);
49
- }
50
37
  export function classifyRulesFailure(e) {
51
- const { status, errorCode, message } = errShape(e);
38
+ const { status, errorCode, message, retryAfterSec } = wireFailureShapeOf(e);
52
39
  if (errorCode === 'capability.rule_store_required') {
53
40
  return { kind: 'lane-unavailable', message };
54
41
  }
@@ -60,7 +47,7 @@ export function classifyRulesFailure(e) {
60
47
  return { kind: 'error', message };
61
48
  }
62
49
  if (errorCode === 'state.rule_import_retry') {
63
- const sec = retryAfterSecOf(e);
50
+ const sec = retryAfterSec;
64
51
  return { kind: 'retry-same-ticket', message, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
65
52
  }
66
53
  if (errorCode === 'state.rule_remove_failed')
@@ -1,3 +1,4 @@
1
+ import { wireFailureShapeOf } from '../wireFailureShape.js';
1
2
  import { hostLog } from '../host.js';
2
3
  const RULE_FIELD_PRESENCE = {
3
4
  toolAllow: true,
@@ -8,16 +9,8 @@ const RULE_FIELD_PRESENCE = {
8
9
  };
9
10
  export const SESSION_POLICY_RULE_FIELDS = Object.freeze(Object.keys(RULE_FIELD_PRESENCE));
10
11
  const MAX_RULE_ENTRIES = 10_000;
11
- function errShape(e) {
12
- const o = (e ?? {});
13
- return {
14
- ...(typeof o.status === 'number' ? { status: o.status } : {}),
15
- ...(typeof o.errorCode === 'string' ? { errorCode: o.errorCode } : {}),
16
- message: typeof o.message === 'string' && o.message !== '' ? o.message : String(e),
17
- };
18
- }
19
12
  export function classifySessionPolicyFailure(e) {
20
- const { status, errorCode, message } = errShape(e);
13
+ const { status, errorCode, message } = wireFailureShapeOf(e);
21
14
  if (errorCode === undefined || errorCode === '')
22
15
  return { kind: 'no-verdict', message };
23
16
  if (status === 501 || errorCode === 'capability.session_store_required') {
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export * from './sessionPolicyCapability.js';
30
30
  export * from './memoryComplianceCapability.js';
31
31
  export * from './memoryOriginCapability.js';
32
32
  export * from './memoryEntriesWire.js';
33
+ export * from './memoryVerbsWire.js';
33
34
  export * from './deviceExecutorManagementCapability.js';
34
35
  export * from './mcpReconnect.js';
35
36
  export * from './leaderConflict.js';
@@ -85,6 +86,7 @@ export * from './effortWire.js';
85
86
  export * from './adapter/types.js';
86
87
  export * from './adapter/downstream/turnUsageToModelUsage.js';
87
88
  export * from './adapter/downstream/eventToSdkMessage.js';
89
+ export type { WiringManifestView, WiringManifestSections } from './adapter/downstream/wiringManifestView.js';
88
90
  export * from './adapter/downstream/terminalToSdkResult.js';
89
91
  export * from './adapter/runStream.js';
90
92
  export * from './adapter/activeRunSelfHeal.js';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export * from './sessionPolicyCapability.js';
30
30
  export * from './memoryComplianceCapability.js';
31
31
  export * from './memoryOriginCapability.js';
32
32
  export * from './memoryEntriesWire.js';
33
+ export * from './memoryVerbsWire.js';
33
34
  export * from './deviceExecutorManagementCapability.js';
34
35
  export * from './mcpReconnect.js';
35
36
  export * from './leaderConflict.js';
@@ -77,7 +77,7 @@ export function mcpLivenessRollupOf(rows) {
77
77
  const epochLabel = (ms) => Math.abs(ms) <= 8.64e15 ? new Date(ms).toISOString() : String(ms);
78
78
  export function mcpEngineLegHealthDetail(health) {
79
79
  if (health === undefined)
80
- return 'mcp liveness not reported (no liveness observation is available to this client)';
80
+ return 'mcp liveness not reported (this client holds no liveness reading for this leg)';
81
81
  const unreadable = health.unreadableCells === true ? ' (one or more liveness records could not be read)' : '';
82
82
  const at = health.observedAt !== undefined ? `, last observed at ${epochLabel(health.observedAt)}` : '';
83
83
  switch (health.reading) {
@@ -91,6 +91,7 @@ export function mcpEngineLegHealthDetail(health) {
91
91
  }
92
92
  return `mcp liveness: observed on this leg, but what came back does not answer whether the server can be reached${at}${unreadable}`;
93
93
  default:
94
- return `mcp liveness not reported (no observation record on this leg's roster)${unreadable}`;
94
+ return 'mcp liveness not reported (no liveness observation is available to this client, and this client'
95
+ + ` cannot tell which cause applies)${unreadable}`;
95
96
  }
96
97
  }
package/dist/mcpPanel.js CHANGED
@@ -90,7 +90,7 @@ export function mcpPanelLastLegDetail(view, opts) {
90
90
  if (view.lastLegMcpUnreadable === true) {
91
91
  return 'last leg mcp unreadable (the engine sent a shape this client cannot read)';
92
92
  }
93
- return 'last leg mcp not reported (no leg in the retention window, no manifest on the last leg, or the engine predates it)';
93
+ return 'last leg mcp not reported (the panel carried no entry for the last leg; this client cannot tell which cause applies)';
94
94
  }
95
95
  const _mcpPanelShapePin = (p) => p;
96
96
  void _mcpPanelShapePin;
@@ -0,0 +1,106 @@
1
+ import type { EntryProvenanceAccount, MemoryErasureRequest, MemoryErasureAttestation, MemoryOriginClearRequest, MemoryOriginClearReceipt, MemoryOriginClearanceRow, MemoryOriginClearancesResult, MemoryOriginExternalEntry, MemoryOriginExternalResult } from '@sema-agent/sdk';
2
+ import type { MemoryComplianceReading } from './memoryComplianceCapability.js';
3
+ import type { MemoryOriginReading } from './memoryOriginCapability.js';
4
+ import type { MemoryEraseReceiptReading, MemoryOriginClearanceReading } from './memoryEntriesWire.js';
5
+ export interface MemoryFacade {
6
+ provenance(entryId: string, opts?: {
7
+ signal?: AbortSignal;
8
+ }): Promise<EntryProvenanceAccount>;
9
+ erase(body: MemoryErasureRequest, opts?: {
10
+ signal?: AbortSignal;
11
+ }): Promise<MemoryErasureAttestation>;
12
+ originExternal(scopes: string[], opts?: {
13
+ signal?: AbortSignal;
14
+ }): Promise<MemoryOriginExternalResult>;
15
+ originClearances(opts?: {
16
+ signal?: AbortSignal;
17
+ }): Promise<MemoryOriginClearancesResult>;
18
+ originClear(entryId: string, body: MemoryOriginClearRequest, opts?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<MemoryOriginClearReceipt>;
21
+ }
22
+ export declare const MEMORY_VERB_REFUSAL_CAUSES: readonly ["face_absent", "client_too_old", "unreadable_request", "store_absent", "operator_only", "unauthorized", "service_token_required", "id_invalid", "request_id_required", "body_shape", "query_invalid", "selector_invalid", "evidence_unavailable", "selector_mismatch", "census_incomplete", "clear_unattributed", "clear_invalid", "clear_not_marked", "clear_challenged", "clear_conflict", "clear_pending"];
23
+ export type MemoryVerbRefusalCause = (typeof MEMORY_VERB_REFUSAL_CAUSES)[number];
24
+ export declare const MEMORY_VERB_UNKNOWN_WHYS: readonly ["no_verdict", "server_fault", "control_plane_corrupt", "clear_unknown", "clear_failed", "result_unreadable", "unsupported_version", "identity_mismatch", "unclassified"];
25
+ export type MemoryVerbUnknownWhy = (typeof MEMORY_VERB_UNKNOWN_WHYS)[number];
26
+ export type MemoryVerbRefused = {
27
+ status: 'refused';
28
+ cause: MemoryVerbRefusalCause;
29
+ message: string;
30
+ };
31
+ export type MemoryVerbUnknown = {
32
+ status: 'unknown';
33
+ why: MemoryVerbUnknownWhy;
34
+ message: string;
35
+ };
36
+ export type MemoryVerbFailure = MemoryVerbRefused | MemoryVerbUnknown;
37
+ export declare function classifyMemoryFailure(e: unknown): MemoryVerbFailure;
38
+ export interface MemoryProvenanceCall {
39
+ entryId: string;
40
+ capability: MemoryComplianceReading;
41
+ }
42
+ export type MemoryProvenanceOutcome = {
43
+ status: 'account';
44
+ account: EntryProvenanceAccount;
45
+ } | MemoryVerbRefused | MemoryVerbUnknown;
46
+ export declare function readEntryProvenance(facade: MemoryFacade, call: MemoryProvenanceCall, opts?: {
47
+ signal?: AbortSignal;
48
+ }): Promise<MemoryProvenanceOutcome>;
49
+ export interface MemoryEraseCall {
50
+ body: MemoryErasureRequest;
51
+ capability: MemoryComplianceReading;
52
+ }
53
+ export type MemoryEraseOutcome = {
54
+ status: 'attested';
55
+ receipt: Extract<MemoryEraseReceiptReading, {
56
+ kind: 'present';
57
+ }>;
58
+ } | MemoryVerbRefused | MemoryVerbUnknown;
59
+ export declare function eraseMemoryEntries(facade: MemoryFacade, call: MemoryEraseCall, opts?: {
60
+ signal?: AbortSignal;
61
+ }): Promise<MemoryEraseOutcome>;
62
+ export interface MemoryOriginExternalCall {
63
+ scopes: string[];
64
+ capability: MemoryOriginReading;
65
+ }
66
+ export type MemoryOriginExternalOutcome = {
67
+ status: 'listed';
68
+ scopes: string[];
69
+ entries: MemoryOriginExternalEntry[];
70
+ } | MemoryVerbRefused | MemoryVerbUnknown;
71
+ export declare function listExternalOriginEntries(facade: MemoryFacade, call: MemoryOriginExternalCall, opts?: {
72
+ signal?: AbortSignal;
73
+ }): Promise<MemoryOriginExternalOutcome>;
74
+ export declare function externalOriginCoverage(outcome: MemoryOriginExternalOutcome, asked: {
75
+ scopes: string[];
76
+ }): {
77
+ kind: 'audited';
78
+ scopes: string[];
79
+ } | {
80
+ kind: 'unknown';
81
+ why: 'not_listed' | 'coverage_shortfall';
82
+ };
83
+ export interface MemoryOriginClearancesCall {
84
+ capability: MemoryOriginReading;
85
+ }
86
+ export type MemoryOriginClearancesOutcome = {
87
+ status: 'ledger';
88
+ clearances: MemoryOriginClearanceRow[];
89
+ } | MemoryVerbRefused | MemoryVerbUnknown;
90
+ export declare function listOriginClearances(facade: MemoryFacade, call: MemoryOriginClearancesCall, opts?: {
91
+ signal?: AbortSignal;
92
+ }): Promise<MemoryOriginClearancesOutcome>;
93
+ export interface MemoryOriginClearCall {
94
+ entryId: string;
95
+ body: MemoryOriginClearRequest;
96
+ capability: MemoryOriginReading;
97
+ }
98
+ export type MemoryOriginClearOutcome = {
99
+ status: 'cleared';
100
+ receipt: Extract<MemoryOriginClearanceReading, {
101
+ kind: 'present';
102
+ }>;
103
+ } | MemoryVerbRefused | MemoryVerbUnknown;
104
+ export declare function clearEntryOrigin(facade: MemoryFacade, call: MemoryOriginClearCall, opts?: {
105
+ signal?: AbortSignal;
106
+ }): Promise<MemoryOriginClearOutcome>;