@sema-agent/client-core 0.81.0 → 0.82.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 +26 -0
- package/README.md +6 -4
- package/dist/adapt/arms.js +3 -1
- package/dist/adapter/activeRunSelfHeal.js +5 -2
- package/dist/adapter/downstream/terminalToSdkResult.js +4 -1
- package/dist/engineErrorCodes.d.ts +1 -0
- package/dist/engineErrorCodes.js +1 -0
- package/dist/gateOutcome.js +28 -2
- package/dist/hitl/hitlHostSurface.js +7 -4
- package/dist/hitl/parkResolver.js +6 -1
- package/dist/hitl/planReviewWire.js +7 -0
- package/dist/hitl/toolApprovalWire.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/registryQuotaUsage.d.ts +44 -0
- package/dist/registryQuotaUsage.js +111 -0
- package/dist/rewindArchiveCapability.d.ts +24 -0
- package/dist/rewindArchiveCapability.js +125 -0
- package/dist/toolRoster.d.ts +12 -0
- package/dist/toolRoster.js +35 -0
- package/dist/wireErrorTriage.d.ts +4 -0
- package/dist/wireErrorTriage.js +59 -5
- package/dist/wireFailureShape.js +27 -22
- package/docs/INTEGRATION-CLIENTS.md +112 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,32 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.82.0(2026-09-23)
|
|
53
|
+
|
|
54
|
+
### 🔴 删键预告改期(本版不删,0.83.0 删)
|
|
55
|
+
- 0.81.0 预告「0.82.0 删 23 个键 + 1 个子型」(CC 形消息上本包自铸或从 CC 内部转录面搬来的顶层键,含过渡键 `toolUseResult`)**改到 0.83.0**,按维护方裁定。本版这些键**照旧在场、与 0.81.x 逐字同形**,读它们的端在 0.82.0 上零改动;逐键终态(删 / 改 `_sema_` 前缀 / 映到 SDK 面同义位)与处置清单在 0.83.0 之前单独发布,读点多的键会逐一点名。接入文档 §92y ㉙。
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- **`/rewind` 能不能回退代码,改由引擎自己的能力位回答**(CC-146):新增只读读面 `observedRewindArchive` / `noteEngineCapsForRewindArchive` / `forgetRewindArchiveReading`(`/v1/capabilities` 的 `resumeAt`=能不能按消息分叉对话、`rewindFiles`=分叉同步回退跟踪集、`rewindFilesTo`=只回退代码不分叉、`restoreFiles`=服务端认不认当前请求键拼法四位),判据单源 `rewindCodeArchiveAvailability(reading, 'both' | 'code')` 答 `available` / `unavailable` / `unknown`,以及措辞单源 `rewindArchiveDoctorDetail` / `rewindRestoreFilesEpochDetail`。消费端此前用「本地备份表非空」判这件事,而那张表只由消费端自己进程内的工具填 —— 引擎执行工具的会话上它恒空,于是两个回退代码的档**从不出现**,而引擎那边文件历史一直在记。「回退对话并连同代码」这一档要**两件都在**(对话锚与文件历史),上游只给「只回退代码」那一档合了锚条件、没给这一档合取位,所以合取在判据里做:任一明说没有 ⇒ 不能,全部明说有 ⇒ 能,其余判不出 —— 只看文件历史会在「有文件历史、没有锚存储」的部署上把这一档说成可用,而请求必败。每位四态:引擎说有 / 说没有 / **没报**(老引擎)/ **报了读不出**,后两者一律 `unknown`,既不折成「不能」也不折成「能」;一位读不出不连坐其余几位;`unknown` 的处置是照常给档、不承诺结果。键名由编译期不变量绑在上游 `Capabilities` 的**显式声明**成员上(剥掉索引签名 —— 不剥则该不变量对任何串恒真)。`restoreFiles` 位只进诊断句、**不**改请求键拼法(旧拼法在现役引擎上被直接拒)。同票追加「只回退对话」那一档的判据 `rewindConversationAvailability(reading)` 与措辞单源 `rewindConversationDoctorDetail(reading)`:只看对话锚位,三态同上;锚明说没有时两个回退到消息的档一起不可用,锚有也不代表「连同代码」那一档可用。「连同代码」那一档在「没有文件历史」时的诊断句不再顺带断言「对话还能回退」—— 对话锚判不出时那句话没有依据,对话能否回退由新判据回答。接入文档 **§92**。
|
|
59
|
+
|
|
60
|
+
- **续跑被拒时,判断按「原本是什么错」而不是「被折成了什么错」**(CC-154):引擎自 7.95.0 起把几条**续跑路径**(审批卡作答后的续跑、唤醒、计划复核等,都从提交时保存的上下文重建请求)上的受理拒绝**统一折**成一个「受理策略变了」的错误,**原本的错误码被放进另一个字段**,附加材料原样随体。新增取码单源:外层正是那只折叠码且里层带着原码时**答原码**,否则答外层码 —— 折叠在与不在**同一份读法**,上游把折叠收窄回去那天本包零改动跟随;外层不是那只折叠码时,里层同名字段**不夺话语权**。受它影响的现有读口有一处:「场景不在你的指派列表」那句专属文案 —— 挂起与续跑之间收紧了场景指派时,它此前在续跑路径上认不出、用户只拿到一句兜底错误,现在经取码单源读得出原码与可用列表;豁免只给折叠形,非折叠的同状态错误照旧不认。**射程**:主对话提交不经这只折叠,那条路径上「恢复点失效就去掉它自动重发一次」的行为不受影响。接入文档 **§92**。
|
|
61
|
+
- **装配期就能知道「这一 run 有没有手」**(CC-152):新增 `handsMountedFromManifest` / `handsMountedDetail`,并给工具名册的行加一格 `mountedBy`(「是哪个挂载条件让它上车」;上游闭集十一词,当前 SDK 未声明这一位、按结构读、认词不收窄)。装配清单里**一行带这一条件的工具都没有**,意味着**这一条腿**上模型手里**没有引擎自带的**改文件 / 跑命令工具(清单按腿铸:委派出去的子腿可能挂了这类工具,由别的条件挂上来的外接工具也可能写盘 —— 这一读数不覆盖它们,也不断言改盘不可能);而这件事在**终态层说不出口**:引擎的终态词汇说的是「这趟跑完了」,不回答「做成没做成」,所以等到终局再判的编排器无从判起。读数三态,两个方向都不折:名册**读得出**且这样的行恰零条,是上游在陈述一个事实;而**根本没有名册**不是那个事实 —— 清单的静态半场从不带名册,更老的引擎给名册但不报挂载条件,那里的「零条」是关于读者的陈述而不是关于这趟跑的。四种判不出各有各的说法,每一句都不会说成「这趟没有工具」。接入文档 **§92**。
|
|
62
|
+
- **云控制面额度读数的三分**(CC-150):新增 `projectRegistryQuotaUsage` / `quotaAmountText` / `registryQuotaDoctorDetail`。`GET /api/v1/quota/usage`(principal 视图)的回体里 `null` 有**两种正面事实**并且互不相同 —— `tokenQuota: null` = 本实例没给这个主体配额度、`limit` / `remaining: null` = 此窗不设限;而**键缺席或类型不对**才是判不出。三者在读数上分得开,**没有一档折成 `0`**(措辞里 `no cap` 与 `unknown` 两句零个数字):把「不设限」渲成 `0` 会让用户以为一滴不剩,把「没配额度」渲成判不出会让这条真话消失 —— 而本票的起因正是命令行**看不到**中心配好的额度。`used` 没有「不设限」这一档(上游恒给数),那一格上的 `null` 只能判不出;`resetAt: null` 只有在上游明说「没耗尽」时才是「没有恢复时刻」这条事实,否则只是判不出(耗尽与否与有没有恢复时刻是两个判断,不共用一位)。取回体那一步仍在调用方(子路径入口是零加工转口面,不放带判断的读器)。回体里的 `budget` 与 `latestRequest` 本版不投影(缺席语义未读到铸点)。接入文档 **§92**。
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
- **几只错误读口在「附加键」读不出时不再抛出**:从错误对象上取附加字段(场景拒绝的允许列表、续跑拒绝里的等待秒数与运行 id、折叠码背后的原码、读不出的那一行坐标)的那一只共用读法,此前在对象被撤销的代理上会把异常抛进调用方的错误处理路径;现在读不出一律当缺席。
|
|
66
|
+
- **会话存档的续跑上下文读不出时,不再丢掉引擎点名的那一行**(CC-156):引擎此时回答「这一会话存下的续跑上下文读不出」并在 `where` 里给出读不出的那一行,原句还叫人去看 `where` —— 而计划复核的决断、耐久审批的批准 / 拒绝、提问卡的作答,用户中断时撤掉挂起卡片的那一次拒绝,以及忙碌会话里选择「插话」而那一轮恰好已经结束的那一次,这几条路径此前只转述那句话、把坐标丢了;turn 错误判决的 `http` 臂也只投状态、码与原句。新增读口 `corruptStoredRowWhere(err)` 与措辞单源 `corruptStoredRowContent(where)`(坐标先转义控制字符再封长),turn 错误判决的 `http` 臂多一格 `storedRowWhere`(出现即非空,值为原文,渲前需转义);上述几条路径的失败说明(中断那一条是上屏的警告与日志)现在都带出那一行;读不出坐标时逐字同旧。计划复核这一形同时改判为「没有生效」(引擎在读到上下文之前不会落定这次决断),不再说「无法确认」。已知限制:审批卡与提问卡在同步车道上仍以原终帧收尾,这时坐标只写进宿主日志,屏上看不到(KNOWN-LIMITS KL-40)。接入文档 **§92 S-12**。
|
|
67
|
+
- **自动模式分类器拒掉的工具调用,不再被标成「规则拒」**(CC-155,订正 0.81.1):0.81.1 给「没问过就拒」的调用补上被拒类别时,把分类器那一层也并进了 `permission-rule`。现按门记录里拒的**形**分三词:分类器自己的裁决 ⇒ `automode-blocked`,分类器没跑成而 fail-closed ⇒ `automode-unavailable`,分类器答了但读不出裁决 ⇒ `automode-parsing-error`;由上游策略链带着分类器成因拒的,同按分类器词;成因词不认识或读不出 ⇒ 整键缺席不猜(只在分类器与策略链两层上;其余层照旧按层名);这些只在「帧上没有本地类别、门记录里没有结算」时适用,结算词与本地类别照旧优先;策略链的拒不带成因、却带着分类归属时,记录分不开「分类器自己判的」与「分类器放行后别的策略拒的」⇒ 整键缺席;其余各层照旧 `permission-rule`(已知限制见 KNOWN-LIMITS KL-39)。user 工具结果记录与终帧被拒清单同一只读数,交互与 `--print` 两条车道同批受益。在 0.81.1 上这类拒绝会被说成一条规则拦的,用户会去找一条不存在的规则;更早的版本上它是缺席。接入文档 **§92 S-10 / ㉘**。
|
|
68
|
+
- **文档订正:「工具结果两键同缺席」这一读法作废**(§92y ㉗;§90b 第一条与 §90c U-G1 后半):结果两键只在有值时铸,而回落链在空输出上产出空串 ⇒ **两键恒在场**。空输出有两种值形 —— wire 既无结构化输出又无正文 ⇒ 值 `''` 且带降级标记;正文是空串 ⇒ 值是该工具的退化结构体、不带降级标记。按「键在不在」判「这张卡有没有结果」两种都会误判;新判据看**值**与降级标记。本版无代码改动,判据与文档改口。
|
|
69
|
+
|
|
70
|
+
## 0.81.1(2026-09-22)
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- **引擎「没问过就拒」的工具调用在 user 工具结果记录上不带被拒分类词 `_sema_denial_kind`**(CC-148):被拒分类词的读器此前只认两路 —— 本包自铸位、引擎结算词(`gate.settlement.kind` 的 `human_refused` / `policy_refused`);而 SDK 契约写明 `settlement` 只在这次通过真的结算了一只 ask 时在场,deny 规则 / 权限模式 / hook / 分类器 / 写保护的拒**根本不问** ⇒ 无 `settlement`,只有 `gate.disposition.deniedBy` 说是哪一层 ⇒ 交互与 `--print` 两条车道上这一大类拒绝恒缺席(交互车道实测:人 / 壳拒的记录两名在场,规则拒的记录两名缺席)。现读器加第三路:`settlement` 键缺席 + `deniedBy` ∈ 闭集且不是 `ask_resolution` ⇒ `permission-rule`;`ask_resolution` 却无结算(违「同在同缺」)、结算段在场但不成形、层名表外 ⇒ 仍整键缺席不猜;结算词在场时结算词优先。交互车道的收口臂在帧上没有自铸位时回落到同一只读器(此前只搬自铸位)。`deniedBy` 照读 wire 层名不改口。终帧 `permission_denials[].toolDenialKind` 由同一份读数 join,同批受益。
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
- **终帧两臂 `_sema_effective_*` 键集不变量**:成功臂展开单铸函数、错误信封显式重发两键 —— 后者是名单写法,给单铸函数加第三键而漏了错误臂会一臂静默少键;现覆盖性钉在**返回对象自己的类型**上(`Exclude<keyof EffectiveFactParts, keyof typeof out>` 与反向各须为 `never`,漏键 / 多键 tsc 当场红 —— 不是另写一份会腐烂的名单)+ 门格判「同一份记录两臂键集相等、值逐字节同」。键名仍字面写出,因为消息键普查门按语法穷举顶层键,循环 / 计算键它判不了。
|
|
77
|
+
|
|
52
78
|
## 0.81.0(2026-09-22)
|
|
53
79
|
|
|
54
80
|
### 🔴 BREAKING 预告(本版不删,0.82.0 删)
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.82.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
|
|
@@ -303,14 +303,14 @@ guard still cross-checks the table by name).
|
|
|
303
303
|
| `scripts/run-terminal-cause-projection-test.mjs` | The `7.64.0` wire reshape, projected. A run's ending stopped being eight parallel flat keys and became **one tagged cause** (`completed \| failed \| blocked \| paused`), and a tool call's gate stopped being four orthogonal words and became **one record** (`disposition` / `settlement?` / `origin?`). Both are read in exactly one place in this package, and this guard pins them at **two levels**, because the dangerous seam is "the reader was updated, the consumer was not": each terminal arm is checked on the reader *and* on the `subtype` / `is_error` / `errors[]` the projector actually emits. Two properties carry most of the weight. First, a terminal word this reader does not know is **never** laundered into an empty success — it lands on an `unknown` arm carrying the word verbatim, while a payload with no terminal word at all (the mock lane) keeps the success arm exactly as before, which is the one and only case the reader answers `null`. Second, the three window words (`approval_window_expired`, `denial_limit_window_expired`, `park_sla_expired`) must each be told apart by a different predicate: the previous generation collapsed all three onto one `timeout`, and re-merging them would throw away the discrimination this reshape just restored. Two byte generations are read by one reader, keyed on the discriminator upstream nailed (`"terminal" in result`): the current cause form, and the **flat** form that a current engine still emits on two lanes — replayed persisted bytes, which the service passes through verbatim rather than back-filling, and the service's own rejection envelope. A cause-form payload that also carries stale flat keys must ignore them entirely: keeping one compatibility read is what gives a single fact two sources. The same file also pins the MCP delivery verdict and HTTP status riding the wiring manifest, the four-state write-protection reading (where three of the four states mean *cannot tell*, and none of them may be printed as "there is no table"), and the park-reopen fetch identity: that predicate is asserted through the **real entry point**, since the defect being fixed was precisely a call site wired to a different predicate than the one that routed the row there From 0.80.0 one of those three boundaries flips: the key naming **who settled a refusal** stopped being a dead byte and became part of the wire, so the check stopped scanning the build output for the word and started reading the request bodies the two decision legs actually send. A refusal attributed to the deployment's own policy carries the word; one attributed to a person, one with no attribution at all, and one carrying a word the vocabulary does not hold carry nothing — the wire has no slot for “a person decided this” other than the key's absence, so inventing one would be minting a word upstream does not have. The allow family never carries it on any of its routes, because that combination is refused before the approval is judged while the side effects of allowing have already landed, and the three refusals nobody was asked about (a card that failed, a user who walked away, an interruption) carry nothing either. A deployment that signs the bodies it accepts does not sign that word, and there is no capability bit to ask beforehand, so a refusal on exactly that ground is answered by re-sending the same decision once with that one key removed — byte-for-byte the same otherwise — rather than letting an optional note take the whole denial down with it. The guard measures that along three axes: the decision still lands and is reported as decided with the attribution handed back and a separate flag saying it never reached the wire; a caller who aborted in between gets no second request; every other refusal code, and every decision that never carried the key, send exactly once. The classification of a second failure is made from what the second body actually carried, not from what the card asked for. |
|
|
304
304
|
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. The cause table is a **copy**, reconciled word for word in both directions against the installed engine's own bytes — it narrowed upstream, and the guard follows rather than keeping the old shape: a table checked against something nobody ships any more is the oldest way for a guard to be green and wrong. The retirement is held from both sides — the removed table must really be gone upstream, and the removed reader and word must really be gone here — while the word that left keeps arriving cleanly from an older engine, because the reader takes the cause as an **open set**: the vocabulary belongs upstream, so a copied list here would discard a legal value the day one is added, and the value discarded is precisely "this outage is a NEW kind". The reader's one exclusion is the word the engine says it never stamps here — the classifier did run and did answer, just outside its contract, so reading it as a failure would invent an event the engine denies. That exclusion used to be derived from a second table which no longer exists; the reason for it never lived in that table, so it is now stated where it actually comes from, pinned as a **named** set (a magic literal scattered through the reader reds) and cross-checked against the engine's own verdict declaration and against the reader having exactly one such comparison. One reader serves both the live ask and its durable parked twin, since the two carry the same key path and a second copy is how two ledgers drift apart. Absence is pinned as absence — most asks never consulted a classifier at all — and the sentences are checked mutually distinct, prototype-safe, and walked end to end: an unknown word reaches the sentence a person reads (the fallback that names it verbatim) and the status reading (unavailable for this round, never a fallback to "available"), with counter-controls proving neither assertion is vacuous |
|
|
305
305
|
| `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails |
|
|
306
|
-
| `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever |
|
|
306
|
+
| `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever. One reading here answers a question that the terminal state structurally cannot: whether this run was assembled with any file-and-shell tools at all. The engine's terminal vocabulary says a run finished, not whether the work got done, so an orchestrator that waits for the end and then guesses has nothing to guess from — while the assembly manifest already said it at the start, one row per mounted instance with the single condition that mounted it. The reading is three-state and both folds are refused: a roster that is readable and carries no such row is the engine stating a fact, while no roster at all is not that fact — the static half of a manifest never carries one, and an older engine reports rosters without naming the mount condition at all, where an empty count would be a statement about the reader rather than about the run. Those two are kept apart in the reason the reading carries, and the wording for every unknown case is checked never to claim the run had no tools |
|
|
307
307
|
| `scripts/run-permission-rule-issue-codes-test.mjs` | The rule-lint refusal codes an engine reports when it will not compile a permission rule. The SDK publishes neither a schema nor a type for them, so the package mints the table from the engine's own bytes and the guard pays the cost of that copy instead of leaving it to somebody remembering: it parses the codes the engine actually mints and reconciles them against the table in both directions, so a code added upstream (the user would see a bare code) and a code only the package believes in (a branch that can never fire) both fail. It also reconciles the table plus a small retired ledger against the engine's declared union, which is deliberately not the same set — one member was renamed and its old name is still declared — so reviving a code the engine will never mint again is impossible and a future stale member shows up immediately. Sentences are pinned one per code, mutually distinct, and split by family: a rule that is wrong and a rule that is legal but unsupported on this lane are different next steps and may not share a sentence. The engine's own message rides along as prose — sanitized and capped after escaping, never matched on |
|
|
308
308
|
| `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, nine words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the gate parses the members straight out of the installed SDK's declarations and reconciles them against the package's tables in both directions, so a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because the sets differ in kind: one is genuinely closed on the wire (an out-of-set record is withheld by the engine, so reading one means the record is damaged) while the other is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine) Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer A fifth table joins them from 0.80.0: the thirteen words for **how a wait ended**, mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the thirteen words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of the thirteen also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. |
|
|
309
309
|
| `scripts/run-engine-identity-test.mjs` | The engine generation anchors on `/health` (`pid`, `instanceId`, `startedAt`; engine >=7.67.0). `/health` is the one unauthenticated door and its heartbeat is always green, so "another host restarted the shared engine" used to be discoverable only by having some authenticated request hit a 401 first — a path that misreads a restart as a network fault. The reader narrows each anchor independently (one malformed field never hides the other two) and always hands back a reading object rather than an absence, because the caller is asking which anchors answered, not whether there was a response. The comparison is a three-word verdict, not a boolean: `unknown` when the two readings share no comparable anchor at all — an empty intersection means nothing could be compared, never that nothing changed — and the boolean convenience is pinned so that only `true` is an assertion. Any comparable anchor differing decides `changed`, so a reading whose `startedAt` matches while its `instanceId` does not cannot be waved through as the same life; precedence only decides which anchor gets named in the diagnosis |
|
|
310
310
|
| `scripts/run-posture-knob-projection-test.mjs` | The three deployment knobs on the operator face (`serverGates.durableApproval` / `streamAskWindowMs` / `sessionAutoTitle`, engine >=7.67.0), each read as a value **plus who set it plus one operator-facing pointer** rather than a bare value — a bare boolean cannot answer why this particular machine is on this setting or how to pin it back, and a default that flips with the deployment shape is invisible without that. A worker too old to report readings still sends a bare boolean; the reader folds it into the same shell so consumers keep one branch, but raises a `legacy` bit, answers `undefined` from the machine-readable source accessor, and mints a sentence that contains no source word at all — claiming a source nobody reported is worse than admitting the worker cannot say. The other two knobs are honestly absent on such a worker rather than defaulted, a malformed side knob drops only itself while the anchor knob drops the whole reading, and the four sentences are pinned literally distinct so an operator can tell "not observed" from "not reported" from a real value. The last leg reads the installed SDK's `openapi.yaml` and `types.d.ts` directly, including a pin that exactly one knob on this face is numeric — the premise the millisecond-to-prose rendering rests on |
|
|
311
311
|
| `scripts/run-terminal-facts-projection-test.mjs` | The four unconsumed terminal-receipt facts: `TaskResult.effectiveReasoning` / `effectiveMemoryScopes` are narrowed into `_sema_effective_reasoning` / `_sema_effective_memory_scopes` on the CC-shaped `result` (success and error envelopes alike; a malformed value mints nothing, never a default tier), the resume **reopen** family (`resume.env_failed` / `tool_unavailable` / `tool_contract_mismatch`) is a frozen closed set with a reader and three-sentence copy that is disjoint from the refusal and retry-later sets, and `routePairingVerdict` reads `ModelInfo.routePairing` as ok / broken / unknown without policing the open set. A fifth section pins the structured-output key on the success result: the CC-spelled `structured_output` is the only home for the value the wire calls `structuredOutput`. The camelCase spelling this package used to mint on its own — a misspelling of the CC field, not an additive field of our own — rode alongside it for exactly one release (0.79.1) and is **absent from 0.80.0 on**, pinned both by own-key and by `in`, so a consumer still reading the old name sees `undefined` rather than a stale copy. The wire position is read exactly once, so a value-changing accessor is only ever asked for its first answer; absence stays absence; a wire key that is present but `undefined` mints nothing, since a key whose value is `undefined` makes a consumer that tests presence read "the engine produced nothing" as "the engine produced an empty result"; falsy-but-present values such as `null`, `0`, `""` and `false` are still minted, and so are shapes that are not records at all — an empty array, a populated array, a string, a number, a boolean — each carried through by the same reference, because the shape of that value is decided by the caller's own schema and the package does not get to filter it; and the error envelope carries no such key, because the CC error arm has no such field. Which spelling CC itself declares is witnessed from the mirror's own syntax tree rather than a constant copied into the guard, so the day that field is renamed upstream the guard says so. |
|
|
312
312
|
| `scripts/run-export-liveness-test.mjs` | Every runtime export in the public baseline must be **alive**: referenced by some gate, or explicitly registered in `scripts/export-liveness.json` as `contract` (consumed by a client with no gate yet), `internal` (an internal helper amplified onto the public surface by `export *`), `candidate` (with ticket + retire-by) or `retire` (dead; retire-by version). Registration is accounting, not exemption: a row for a name a gate already references is stale and must go, a row for a name no longer exported is red, `retire`/`candidate` rows go red the moment `package.json` reaches their retire-by version, and the row count only ratchets down. When the sibling client trees are on disk the consumption evidence is checked by name — a `contract` row's claimed consumers must equal the real set, and a `retire` name must not be imported by any client. Names that have already left the surface are kept in a per-version `removed` ledger: they must never reappear in the baseline or the registry, and the ledger's versions must not run ahead of the changelog. |
|
|
313
|
-
| `scripts/run-wire-refusal-copy-test.mjs` | Two wire refusals read the same way on every client: a cancel's 409 carries one of two codes with opposite dispositions (`conflict.approval_settled` — someone else already decided, go read the result; `conflict.run_not_running` — nothing changed, send the cancel again), an unrecognised or codeless 409 is reported as such rather than guessed, and the submit-side 429 `usage.window_exhausted` is read as a waitable refusal whose wait is stated only when the engine supplied one. `ControlRouter.cancel` raises a distinct safety code for the retry-directly case. A third family covers the refusals that a deny's *settler note* can draw: a deployment that signs the decisions it accepts but does not sign that note, a body whose decision and note contradict each other, and a word the engine does not recognise. All three are refused **before** the approval is judged, so each sentence states plainly that nothing was decided and the approval is still waiting, and each names a different next step — none of them “send it again unchanged”, which would simply be refused again. Two of the three codes already mean something else in this package: one is shared with the plan-review leg, so the reader refuses to claim it unless the caller states that the note really was sent, and the other is split by the field the engine names, because without that field the same code means a review outcome was rejected for its content. The third sentence deliberately does not say the word was misspelled: upstream mints that same field for at least four different reasons, so the only thing it proves is that the engine would not take the settlement details and named which part — which is what the sentence says, and where it points. The three sentences are pinned verbatim rather than by keyword, because a keyword check passes a sentence that tells the reader to send the same body again unchanged, which is the one next step that is certainly wrong. The field itself is read from the bag the SDK keeps additional response keys in, not off the top of the error, since only the hand-built shapes a test would write carry it there. Both decision legs hand the reading back on their outcome, and a leg that never sent the note claims nothing. |
|
|
313
|
+
| `scripts/run-wire-refusal-copy-test.mjs` | Two wire refusals read the same way on every client: a cancel's 409 carries one of two codes with opposite dispositions (`conflict.approval_settled` — someone else already decided, go read the result; `conflict.run_not_running` — nothing changed, send the cancel again), an unrecognised or codeless 409 is reported as such rather than guessed, and the submit-side 429 `usage.window_exhausted` is read as a waitable refusal whose wait is stated only when the engine supplied one. `ControlRouter.cancel` raises a distinct safety code for the retry-directly case. A third family covers the refusals that a deny's *settler note* can draw: a deployment that signs the decisions it accepts but does not sign that note, a body whose decision and note contradict each other, and a word the engine does not recognise. All three are refused **before** the approval is judged, so each sentence states plainly that nothing was decided and the approval is still waiting, and each names a different next step — none of them “send it again unchanged”, which would simply be refused again. Two of the three codes already mean something else in this package: one is shared with the plan-review leg, so the reader refuses to claim it unless the caller states that the note really was sent, and the other is split by the field the engine names, because without that field the same code means a review outcome was rejected for its content. The third sentence deliberately does not say the word was misspelled: upstream mints that same field for at least four different reasons, so the only thing it proves is that the engine would not take the settlement details and named which part — which is what the sentence says, and where it points. The three sentences are pinned verbatim rather than by keyword, because a keyword check passes a sentence that tells the reader to send the same body again unchanged, which is the one next step that is certainly wrong. The field itself is read from the bag the SDK keeps additional response keys in, not off the top of the error, since only the hand-built shapes a test would write carry it there. Both decision legs hand the reading back on their outcome, and a leg that never sent the note claims nothing. When a decision fails because the engine cannot read a stored row, the row the engine names is read (from the error's top level, or from the SDK's bag of additional response keys where the real SDK puts it) and carried into the failure reason on all three decision legs, escaped for display. |
|
|
314
314
|
| `scripts/run-tool-disclosure-progress-projection-test.mjs` | The two wire arms sdk 9.6.0 adds — `tool_disclosure` (name-only tool census: open-set `policy`, `thresholdPercent` absent ≠ default, `deferred`/`activated` full snapshots) and `tool_progress` (one frame, two beats: Bash ticks carry an output tail with `totalLines`/`totalBytes` that come and go together; other tools carry only `elapsedSeconds`) — project to neutral internal arms plus chrome arms. Required keys missing ⇒ `malformed`; bad optional keys drop only themselves; the sub-flow three-key gate keeps child frames off the leader lane; both arms are `required: false` in the arm table with duties stated (the output tail is untrusted raw and must never be fed back to the model). |
|
|
315
315
|
| `scripts/run-mcp-panel-projection-test.mjs` | The `GET /v1/sessions/:id/mcp` panel reader (`projectMcpPanel`; server >=7.77.0 adds the optional `lastLegMcp` key) and the single wording mint for its "last leg" line. Absence of `lastLegMcp` is one literal sentence that never blames the engine version (a new session, a leg outside the retention window, a leg without a manifest and an older engine all look the same on the wire); a key that is present but unreadable is a different sentence plus a `lastLegMcpUnreadable: true` mark, never folded into absence. The `mcp[]` roster goes through the same reader as the live `wiring_manifest` third section, so a replayed roster and a live one have one shape. The two faces of the panel (`servers[]` and the last-leg roster) may legitimately differ, so the view carries no agreement flag and none of the five sentences mentions `servers`. Required keys are pinned to the SDK `openapi.yaml` component bytes **0.69.0:** `fetchMcpPanel` fetches the panel through the SDK client's own `sessions.mcp` call (same transport and auth as every other read) and projects it; transport failure, an unreadable body and an empty session id all come back as `undefined`, never as a fabricated empty panel 0.71.0 adds section K: `mcpEngineLegPresence(view)` — the engine-side MCP presence tri-state read only off the panel view (`unknown` when the view could not be read, never rendered as "no MCP configured") |
|
|
316
316
|
| `scripts/run-absence-fold-census-test.mjs` | A package-wide census of the "absence folded into a positive outcome" defect shape, so that fixing the six sites this release does not merely move the shape somewhere else. The defect is defined by position, not syntax: a fallback position (the unconditional tail return, the `default:` arm, the literal minted when there is nothing to pass on, the value returned from an error path) may only say `unknown` or stay absent, never a positive word. Detection walks the syntax tree of every source file, so comments, strings and multi-line spellings cannot hide or fake a hit, and covers five forms: the right arm of `??` / `\|\|`, the else arm of a ternary, the first return of an explicit `default:`, a `catch` block or `.catch(() => …)` arrow returning a healthy value, and a function whose last statement returns a positive word after other returns. Every remaining hit must be registered with a written reason, an unregistered hit fails the gate naming the file and line, the registered count must equal the real count so a cleared site cannot leave a spare allowance behind, and the gate proves its own teeth behind a fence (a failed self-proof refuses to report any count): each form injected into an in-memory copy must add exactly one hit, two correct spellings are pinned as non-hits, and samples inside comments or strings do not count. It also pins the headline site: the fleet panel projection no longer mints an `end` with `isError: false` on absence |
|
|
@@ -348,7 +348,7 @@ guard still cross-checks the table by name).
|
|
|
348
348
|
| `scripts/run-background-view-test.mjs` | `createBackgroundView` lifecycle: polling/notify pairing, per-source degrade (`501 → not-configured` vs `unavailable`), the capabilities `scheduler` probe, and dispose really aborting the in-flight fleet snapshot (pure projection lives in the pure suite's W-A segment) |
|
|
349
349
|
| `scripts/run-fleet-view-keys-test.mjs` | The fleet projection views, **both directions**: `FleetTaskView`/`FleetWorkflowView` ⇄ their key lists (compile-pinned) ⇄ what a maximal/minimal row really projects, plus a wire-key coverage ledger (every `FleetTaskRow` key is either projected or carries a written reason why not) and a drift ledger against the shell's render contract. A one-directional assignability check is blind to optional keys — which is how `startedAt` was silently dropped |
|
|
350
350
|
| `scripts/run-usage-verbatim-channel-test.mjs` | The two complementary usage disciplines (core 3.0.0 metering semantics): the CC `ModelUsage` mirror stays pure (five pinned keys, `totalInputTokens` has no seat), while the sema-owned channel forwards the engine `turn_end.usage` object **verbatim** (six keys, incl. `totalInputTokens`) via `last_turn_usage.engineUsage` / `handle.latestEngineUsage` — honest absence on pre-3.0.0 engines, no fabricated zeros |
|
|
351
|
-
| `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned. Driven against a real fake-engine HTTP server through the shipped dist |
|
|
351
|
+
| `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned; when the engine answers that the session's stored resume context cannot be read, the outcome names the unreadable row and says the decision was not applied. Driven against a real fake-engine HTTP server through the shipped dist |
|
|
352
352
|
| `scripts/run-shell-gate-durable-allow-test.mjs` | #110: the durable approval leg for **shell** gates. The tool_end HOLD/REJECT predicate must cover Bash the same way park detection already does (otherwise the park poison frame `Operation aborted` hits the transcript, `endedCalls` swallows the real replayed result, and the user who pressed Yes watches a command that really ran be reported as aborted); a replayed, already-decided park must resume reading the stream instead of being reported as a failed turn; `lastEventId` must track numeric `seq` too. Mutation-proven: each of the three fixes reverted turns the gate red |
|
|
353
353
|
| `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the #110 disease, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live. (6) The live-frame leg carries the fact behind "you are being asked because the auto-mode classifier could not run" all the way to the card port. Transit narrows on SHAPE only — a non-empty cause string is taken verbatim, an open set, because the word table's owner is the engine and re-checking a closed table at the package boundary would drop a legal value the day a new cause word appears, which is exactly the information worth keeping. A malformed carrier degrades to absence rather than half-minting, and absence stays absence: it covers "the classifier answered", "this ask never qualified" and "this deployment has no classifier" at once, so nothing may render it as reassurance. The guard also pins the division of labour that makes the open set safe — the same word that transits is judged again by the public display reader, which narrows to the availability axis, so a word the engine says it never stamps on this fact renders no sentence while still being visible on the card for triage A later section pins the split this release introduced on the deny close-out frame. Until now every denied tool call was stamped with the same sentence — the one that says *the user* does not want to proceed — including the calls denied automatically on a lane that has no approval surface at all, where nobody was ever asked. The guard drives all three shapes (a person pressed No, a rule settled it, nobody said which) through both close-out arms and the durable park leg, and pins that the third shape is byte-identical to the previous release: an attribution nobody supplied is not evidence for either answer. The rule-settled shape carries the shell's own reason on a second line when there is one and stands alone when there is not, because a blank line where a reason should be reads worse than no line at all. The attribution is read from own data properties only, so neither a polluted prototype nor a getter can make an automatic denial claim a person made it — and the getter case is pinned to never run at all. The transcript classification word is minted only on the two paths where the upstream transcript format really carries one; the three classifier words and the two abort words are left absent, with the abort words pinned against the strings this package actually normalises interruptions to, which are different strings |
|
|
354
354
|
| `scripts/run-park-hop-progress-test.mjs` | L-80: the park re-attach loop budgets **stalled** rounds, not parks. A turn where the model keeps hitting gates and every one of them is really decided (a card was answered, the engine really moved on) must never be cut off by the hop budget — the budget counts consecutive rounds that produced no progress, and "the engine revived and immediately parked again on the same coordinates" is not progress. The three non-progress arms (already-resolved, decide-transport-exhausted, and a re-scan that was adopted but led nowhere) share one same-cause limit instead of one arm having a limit and the others having none, and every non-progress re-attach is announced once through the host callback rather than only to the debug log. When the limit is spent the resolver reads the approval queue once more and puts whatever is decidable in front of the user before it gives up; only when there is genuinely nothing to show does it fail soft, and the terminal message then carries the real cause and a real way out instead of a sentence about a budget. On the self-heal side, a reopen verdict that reports `decidedWithoutCard` — the chain settled the gate by rule, so there was no card to present — is progress, not a reopen failure, and the user is not told their message was NOT sent. Negative control: a genuinely empty queue with a run that never moves still fails soft |
|
|
@@ -415,6 +415,8 @@ guard still cross-checks the table by name).
|
|
|
415
415
|
| `scripts/run-rule-removal-consequence-test.mjs` | The one sentence a rule-removal confirmation surface shows for what removing the rule will actually do: one line per behaviour the rule could have been enforcing, plus a neutral line for when that behaviour cannot be read back, so a caller that hits an out-of-set or missing value never falls back to a specific claim it cannot support. The guard compares the actual output against frozen text rather than merely checking that some string came back, so a dropped word or a swapped clause is caught the day it lands, and the four sentences are pinned pairwise distinct. The line for a rule that was denying something is pinned to say the removal widens what can run rather than echoing the wording used for a rule that asks again — the two are opposite directions, and sharing a sentence between them would tell the person confirming the removal the opposite of what is about to happen. The neutral line is checked from the other side for the same reason: it must not contain a word that belongs to only one of the three behaviours, because that would answer on behalf of a state the caller was unable to determine. The lookup that turns a raw stored or transmitted value into one of the three behaviours reads by strict equality only, proven with a fully trapped proxy and a counting getter to show it never touches a property on whatever it is handed, so a value with a legitimate-looking word sitting on its prototype chain is rejected exactly like any other out-of-set value rather than being unwrapped. A closing self-check mutates one character out of each frozen sentence and asserts the exact-match comparison actually fails on it, so the guard cannot pass by checking only that a string of some kind came back |
|
|
416
416
|
| `scripts/run-mcp-engine-leg-test.mjs` | The two legs behind one row on the MCP detail card. In a two-process setup the servers are hosted by the **engine**, while the Status cell on the card reports **this client's own** connection to them, and the two are independent truths: this client failing to connect does not mean the server's tools are unavailable, and the engine leg on the same screen may be saying it completed an exchange moments ago. Two mints share the work. The first reads one server's liveness off the engine leg as six readings, and its load-bearing distinctions are three. **No roster that could be read end to end** (`null`, `undefined`, anything that is not an array, a length that is not a non-negative integer, a length beyond the scan bound, or rows that could not be read at all with nothing matched) is **not** the same as an **empty** roster, which is the engine leg's positive statement that it declared no servers; the same narrower that feeds this reader answers `undefined` when a non-empty section yields no readable row, so *I could not read it* never turns into *I know it is zero*, and a roster that was not read to the end never produces *this server is not on it*. **Could not be read is not the same as absent**: absence means only that the row carries no liveness key of its own, while a record that is present but unintelligible — a cell that is not an object, a word that is not a non-empty string, or a read that fails outright — is reported as unreadable and is decided before the observation beside it, since a record that may well have said the opposite is no evidence of reachability. **An ambiguous name is answered as ambiguous**: when a roster that was read end to end matches a name more than once, the reader reports the match count instead of picking one, because the upstream contract for the sibling MCP face states that names are not guaranteed unique, and picking optimistically would contradict the worst-fact-first verdict shown on the same screen. When a row's identity could not be read at all the reader makes **no claim about that name whatsoever**, not even a count, since the row it could not read may well be a second one carrying the same name — a row that is plainly not a row, such as a hole in a sparse array, is a different matter and leaves the roster complete. The observed word is passed through **verbatim as an open set**, so a fourth word one day arrives at consumers untouched. The second mint is the one sentence that goes under Status, and it appears **only** when this client's own connection really did fail — the other Status values already tell the truth, and a sentence on top of them would only muddy the verbatim health vocabulary. **The liveness observation outranks the tool roster, and having heard from the engine includes hearing that it could not tell, and hearing something unreadable**: a word meaning *looked and could not tell*, a word this version does not recognise, and a record that arrived but could not be read each get their own sentence rather than falling back to the roster, because falling back would say this client holds nothing at all while the diagnostics page shows that very record. A value that is not a reading at all is a different case and does fall back, since nothing then establishes that the engine sent anything. Only when liveness genuinely cannot speak does the roster get a turn, and the roster itself has four answers — tools were listed, nothing was said, the engine reported zero, and the count could not be read — because **unreadable, absent and zero are three different facts**. No sentence claims that nothing at all has been seen about the server, because on two of the readings that reach the roster the engine has plainly listed it; the sentence for a silent roster states only what this client holds. The nine sentences are pairwise distinct, each one names the engine leg, and **none of them renders the liveness word itself**. Membership of the word list is decided by the one shared predicate rather than a second copy, and the branch over the known words is pinned so that a new word upstream fails the build instead of silently taking the *not recognised* sentence. Neither mint ever throws, whatever a host hands it: every value is taken through one reader that accepts **own properties only** — an inherited key is not a wire fact, and one on a shared prototype could otherwise manufacture an engine observation or suppress a real one — reads each key exactly once, and keeps a read that fails apart from a value that is absent. The roster is walked by index rather than through the array's own `find`, the array test is guarded because it can throw on its own, and a length that overstates itself would otherwise spin forever |
|
|
417
417
|
| `scripts/run-cc-message-key-census-test.mjs` | The one rule behind CC-shaped messages this package emits: every top-level key on a `user` / `assistant` / `result` / `system` message must be a member the CC SDK mirror (`@sema-agent/agent-types`) declares for that same arm, or one of this package's `_sema_`-prefixed additive keys, or a row in a dated transition table that goes red the day its retire version arrives. Mint sites are found syntactically (identifier, quoted and computed `type` names alike) and their key sets are resolved syntactically too — inline literals, both branches of a conditional, the nullish-coalescing and logical or/and operators, `const` initializers and every `return` of a helper — so a type assertion, a `Partial<Pick<…>>` narrowing or a computed name cannot launder a key past the check, while a spread the tool cannot follow (a parameter, a `let`, a member access) fails the tool, never the product. Arms that carry a `subtype` discriminator are checked against that subtype's own member set, so a key declared only for another subtype does not pass on the strength of the arm-wide union. The runtime section drives the real adapter pipeline and pins the tool-result record's SDK-spelled `tool_use_result` (the transitional camelCase twin rides along by reference until 0.82.0). |
|
|
418
|
+
| `scripts/run-rewind-archive-capability-test.mjs` | The read face for whether a rewind can restore the code archive, and the honest three-state answer the ends render from it. The shells used to decide this from a local backup table that only their own in-process tools ever fill, so on any session where the engine runs the tools it stayed empty and the two code-restoring rewind modes simply never appeared, while the engine had been keeping a file history the whole time. The judgement now comes from what the engine itself advertises, and each of the four bits it advertises answers a different question: whether the conversation can be forked at a message at all, whether a fork can restore the tracked set, whether a code-only restore is possible, and whether this deployment understands the current spelling of the request key. The mode that rewinds the conversation and restores the code together needs both of the first two, and the engine offers no single bit for that combination, so the combination is made here: one bit stated off is enough to rule the mode out, both stated on make it available, anything else stays unknown — reading the file-history bit alone would offer that mode on a deployment that keeps file history but has no conversation anchors, where the request can only fail. A bit that is absent means an older engine that never spoke about it, which is not the same as an engine that said no, and a bit reported in a shape this reader cannot read is a third thing again — it is recorded as unreadable rather than quietly filed under "not reported", because those two send an operator to different places. One unreadable bit does not discard its siblings; only a response that is not a capability object at all clears the cell. What the ends get is available, unavailable or unknown, and unknown stays unknown: folding it into unavailable would hide the mode again, which is the mirror image of the bug this replaces. The sentences the doctor row can print are checked to be pairwise distinct and to avoid implying a refusal the engine never made, and the spelling of the outgoing request is deliberately not made to follow the epoch bit, since the older spelling is rejected outright by current engines |
|
|
419
|
+
| `scripts/run-registry-quota-usage-test.mjs` | The projection of the cloud control plane's quota reading, and the three different things a missing number can mean there. This response says `null` in two places and means something different each time: no token quota is configured for this principal on this instance, and this window has no cap at all. Both are **facts the server is asserting**, not gaps in the reading — while a key that is absent or carries the wrong type is a genuine gap. All three have to survive to the screen separately, because folding them is how a user ends up staring at a confident `0`: an uncapped window rendered as if nothing were left, or a deployment that simply never configured quotas rendered as if the quota were exhausted. The complaint that started this was the opposite direction — a centrally configured quota that the command line could not see at all — so the reading also refuses to let an unreadable response masquerade as "no quota configured". Two fields deliberately do not share one signal: whether a window is exhausted and whether there is a recovery time, since the recovery time is only ever populated in the exhausted case and reading its absence as "not exhausted" would answer a question the response never answered. Counts that the server always provides are narrowed no further than the mint: a used counter has no uncapped state, so a null there is unreadable rather than zero. The wording helper carries the only human-facing phrasing, and the sentences for "no cap" and "unknown" are checked to contain no digits at all |
|
|
418
420
|
|
|
419
421
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
420
422
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|
package/dist/adapt/arms.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ccToolDenialKindForToolEnd } from '../gateOutcome.js';
|
|
1
2
|
import { isSubFlowSegmentEnd, SEGMENT_END_IDENTITY_KEYS, snapshotSegmentIdentity, snapshotIsSubFlow } from '../adapter/types.js';
|
|
2
3
|
import { classifyPeerNotification, renderPeerFrameTranscriptText } from '../peerFrames.js';
|
|
3
4
|
import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, noteOwnWorkflowRun, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
|
|
@@ -509,7 +510,8 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
|
|
|
509
510
|
const closeParentRaw = m.parentToolCallId;
|
|
510
511
|
const closeEventId = typeof closeEventIdRaw === 'string' ? closeEventIdRaw : undefined;
|
|
511
512
|
const closeParent = typeof closeParentRaw === 'string' ? closeParentRaw : undefined;
|
|
512
|
-
const
|
|
513
|
+
const stampedRaw = m._sema_denial_kind;
|
|
514
|
+
const closeDenialKindRaw = typeof stampedRaw === 'string' ? stampedRaw : ccToolDenialKindForToolEnd(m);
|
|
513
515
|
yield* cards.close(p, {
|
|
514
516
|
output: m.output,
|
|
515
517
|
structured,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { abortableSleep } from '../abortableSleep.js';
|
|
2
|
+
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
2
3
|
export const PLAN_REVIEW_GATE_KIND = 'plan_review';
|
|
3
4
|
export const PLAN_REVIEW_GATE_KINDS = [PLAN_REVIEW_GATE_KIND, 'dry_run_review'];
|
|
4
5
|
export const ASK_PARK_GATE_KINDS = ['human', 'irreversible_ask', 'policy_ask', 'tool_approval'];
|
|
@@ -45,12 +46,14 @@ function sessionOpts(deps) {
|
|
|
45
46
|
return typeof sessionId === 'string' && sessionId.length > 0 ? { session: sessionId } : {};
|
|
46
47
|
}
|
|
47
48
|
function describeFailure(e) {
|
|
49
|
+
const where = corruptStoredRowWhere(e);
|
|
50
|
+
const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
|
|
48
51
|
if (typeof e === 'object' && e !== null && 'message' in e) {
|
|
49
52
|
const m = e.message;
|
|
50
53
|
if (typeof m === 'string' && m.length > 0)
|
|
51
|
-
return m.length > 200 ? `${m.slice(0, 200)}…` : m
|
|
54
|
+
return `${m.length > 200 ? `${m.slice(0, 200)}…` : m}${rowNote}`;
|
|
52
55
|
}
|
|
53
|
-
return
|
|
56
|
+
return `the engine gave no detail${rowNote}`;
|
|
54
57
|
}
|
|
55
58
|
function readStatus(record) {
|
|
56
59
|
const st = record?.status;
|
|
@@ -206,10 +206,13 @@ function effectiveFactParts(rec) {
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
function reemitEffectiveFacts(f) {
|
|
209
|
-
|
|
209
|
+
const out = {
|
|
210
210
|
...(f?._sema_effective_reasoning !== undefined ? { _sema_effective_reasoning: f._sema_effective_reasoning } : {}),
|
|
211
211
|
...(f?._sema_effective_memory_scopes !== undefined ? { _sema_effective_memory_scopes: f._sema_effective_memory_scopes } : {}),
|
|
212
212
|
};
|
|
213
|
+
const _keySetPin = true;
|
|
214
|
+
void _keySetPin;
|
|
215
|
+
return out;
|
|
213
216
|
}
|
|
214
217
|
function costFactParts(stats, observed) {
|
|
215
218
|
const facts = readRunCostFacts(stats, observed);
|
|
@@ -34,6 +34,7 @@ export declare const STOP_NOT_LOCAL = "stop.not_local";
|
|
|
34
34
|
export declare const STOP_PARKED = "stop.parked";
|
|
35
35
|
export declare const STOP_CONFLICT_CODES: readonly ["stop.park_arbiter_unreachable", "stop.park_resume_won", "stop.not_landed", "stop.not_local", "stop.parked"];
|
|
36
36
|
export declare const OUTPUT_INVALID = "output.invalid";
|
|
37
|
+
export declare const RESUME_POLICY_FOLD_CODE = "resume_blocked_by_policy";
|
|
37
38
|
export declare const RESUME_AT_ERROR_CODE_PREFIX = "resume_at.";
|
|
38
39
|
export declare const REWIND_ERROR_CODE_PREFIXES: readonly ["resume_at.", "rewind_snapshot."];
|
|
39
40
|
export declare function isRewindFamilyCode(code: string | undefined): boolean;
|
package/dist/engineErrorCodes.js
CHANGED
|
@@ -69,6 +69,7 @@ export const STOP_CONFLICT_CODES = [
|
|
|
69
69
|
STOP_PARKED,
|
|
70
70
|
];
|
|
71
71
|
export const OUTPUT_INVALID = 'output.invalid';
|
|
72
|
+
export const RESUME_POLICY_FOLD_CODE = 'resume_blocked_by_policy';
|
|
72
73
|
export const RESUME_AT_ERROR_CODE_PREFIX = 'resume_at.';
|
|
73
74
|
export const REWIND_ERROR_CODE_PREFIXES = [RESUME_AT_ERROR_CODE_PREFIX, 'rewind_snapshot.'];
|
|
74
75
|
export function isRewindFamilyCode(code) {
|
package/dist/gateOutcome.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ccToolDenialKindForSettledBy, isCcToolDenialKind } from './gateVocabulary.js';
|
|
1
|
+
import { ccToolDenialKindForSettledBy, isCcToolDenialKind, isGateDeniedByWord } from './gateVocabulary.js';
|
|
2
2
|
import { GATE_PARKED_ERROR_CODE } from './engineErrorCodes.js';
|
|
3
3
|
function str(v) {
|
|
4
4
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
@@ -146,13 +146,39 @@ export function ccToolDenialKindForToolEnd(frame) {
|
|
|
146
146
|
const stamped = frame._sema_denial_kind;
|
|
147
147
|
if (isCcToolDenialKind(stamped))
|
|
148
148
|
return stamped;
|
|
149
|
-
const
|
|
149
|
+
const outcome = gateOutcomeOf(frame);
|
|
150
|
+
const kind = outcome?.settlement?.kind;
|
|
150
151
|
if (kind === 'human_refused')
|
|
151
152
|
return ccToolDenialKindForSettledBy('human');
|
|
152
153
|
if (kind === 'policy_refused')
|
|
153
154
|
return ccToolDenialKindForSettledBy('policy');
|
|
155
|
+
const rawGate = frame.gate;
|
|
156
|
+
const settlementAbsent = typeof rawGate === 'object' && rawGate !== null && rawGate.settlement === undefined;
|
|
157
|
+
if (outcome !== undefined && settlementAbsent && outcome.disposition.kind === 'denied') {
|
|
158
|
+
const by = outcome.disposition.deniedBy;
|
|
159
|
+
if (by === 'ask_resolution' || !isGateDeniedByWord(by))
|
|
160
|
+
return undefined;
|
|
161
|
+
if (DENIED_BY_MAY_CARRY_CAUSE.has(by)) {
|
|
162
|
+
const rawDisposition = rawKeyOf(rawGate, 'disposition');
|
|
163
|
+
if (rawKeyOf(rawDisposition, 'cause') !== undefined) {
|
|
164
|
+
const cause = outcome.disposition.cause;
|
|
165
|
+
if (cause === 'unavailable')
|
|
166
|
+
return 'automode-unavailable';
|
|
167
|
+
if (cause === 'parse_error')
|
|
168
|
+
return 'automode-parsing-error';
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (by === 'classifier')
|
|
172
|
+
return 'automode-blocked';
|
|
173
|
+
if (rawKeyOf(rawDisposition, 'classifier') !== undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return ccToolDenialKindForSettledBy('policy');
|
|
177
|
+
}
|
|
154
178
|
return undefined;
|
|
155
179
|
}
|
|
180
|
+
const rawKeyOf = (holder, key) => typeof holder === 'object' && holder !== null ? holder[key] : undefined;
|
|
181
|
+
const DENIED_BY_MAY_CARRY_CAUSE = new Set(['classifier', 'policy']);
|
|
156
182
|
const _gateOutcomeShapePin = (w) => w;
|
|
157
183
|
void _gateOutcomeShapePin;
|
|
158
184
|
const _settlementWordMirrorPin = SETTLEMENT_KIND_WORDS;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
2
2
|
import { hostLog } from '../host.js';
|
|
3
3
|
import { unrefTimer } from '../unrefTimer.js';
|
|
4
|
+
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
4
5
|
import { HitlSafetyError } from './hitlBridge.js';
|
|
5
6
|
const hostSurfaceByKey = createSessionSlot();
|
|
6
7
|
const hostSurfaceMissesByKey = new Map();
|
|
@@ -38,8 +39,8 @@ export const CANCEL_DENY_BUDGET_MS = 2000;
|
|
|
38
39
|
export const CANCEL_DENY_WARN_TEXT = 'could not cancel the pending question — the session may stay locked; the run may need engine-side recovery';
|
|
39
40
|
const CANCEL_DENY_WARN_KEY = 'hitl-cancel-deny-warn';
|
|
40
41
|
const CANCEL_DENY_WARN_TIMEOUT_MS = 10_000;
|
|
41
|
-
function surfaceCancelDenyWarn() {
|
|
42
|
-
surfaceSelfClearingWarn(CANCEL_DENY_WARN_KEY,
|
|
42
|
+
function surfaceCancelDenyWarn(text = CANCEL_DENY_WARN_TEXT) {
|
|
43
|
+
surfaceSelfClearingWarn(CANCEL_DENY_WARN_KEY, text, CANCEL_DENY_WARN_TIMEOUT_MS);
|
|
43
44
|
}
|
|
44
45
|
function surfaceSelfClearingWarn(key, text, timeoutMs) {
|
|
45
46
|
try {
|
|
@@ -112,8 +113,10 @@ export function observeCancelByDeny(settle, taskId) {
|
|
|
112
113
|
hostLog('debug', `[hitl-cancel-deny] cancel-by-deny no_pending for run ${taskId} (benign — already resolved)`);
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
const where = corruptStoredRowWhere(e);
|
|
117
|
+
const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
|
|
118
|
+
hostLog('error', `[hitl-cancel-deny] cancel-by-deny FAILED for run ${taskId}: ${String(e)}${rowNote}`);
|
|
119
|
+
surfaceCancelDenyWarn(`${CANCEL_DENY_WARN_TEXT}${rowNote}`);
|
|
117
120
|
})
|
|
118
121
|
.finally(() => {
|
|
119
122
|
if (timer !== undefined)
|
|
@@ -7,6 +7,7 @@ import { observeCancelByDeny } from './hitlHostSurface.js';
|
|
|
7
7
|
import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
|
|
8
8
|
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
9
9
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
10
|
+
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
10
11
|
export const MAX_GATE_HOPS = 2;
|
|
11
12
|
export function nextHopBudget(prev, round) {
|
|
12
13
|
const total = (prev?.total ?? 0) + 1;
|
|
@@ -140,7 +141,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
140
141
|
kind: 'failed',
|
|
141
142
|
stage: 'decide',
|
|
142
143
|
gatedCallId,
|
|
143
|
-
reason: `decide failed: ${String(e)}`,
|
|
144
|
+
reason: withCorruptStoredRow(`decide failed: ${String(e)}`, e),
|
|
144
145
|
...(code !== undefined ? { code } : {}),
|
|
145
146
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
146
147
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
@@ -148,6 +149,10 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
151
|
}
|
|
152
|
+
function withCorruptStoredRow(reason, err) {
|
|
153
|
+
const where = corruptStoredRowWhere(err);
|
|
154
|
+
return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
|
|
155
|
+
}
|
|
151
156
|
function argsByCallOf(led) {
|
|
152
157
|
return new Map(led.gatedStartArgs());
|
|
153
158
|
}
|
|
@@ -3,6 +3,7 @@ import { hostLog } from '../host.js';
|
|
|
3
3
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
4
4
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
5
5
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
6
|
+
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
6
7
|
import { isPlanReviewModeAfter } from './hitlBridge.js';
|
|
7
8
|
import { engineCapString } from '../engineCapsCache.js';
|
|
8
9
|
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
@@ -326,6 +327,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
326
327
|
`This ${decision} was not applied: the engine no longer has a pending plan review for this task — an earlier decision most likely already went through (or the review expired). ` +
|
|
327
328
|
'This is NOT a rejection of the plan and NOT a failure of the task; the answer just given simply had nothing left to act on.';
|
|
328
329
|
}
|
|
330
|
+
else if (typeof status === 'number' && corruptStoredRowWhere(e) !== undefined) {
|
|
331
|
+
const where = corruptStoredRowWhere(e);
|
|
332
|
+
hostLog('debug', `planReviewWire: ${decision} → ${String(status)} corrupt (stored resume context unreadable)`);
|
|
333
|
+
effect = 'not_applied';
|
|
334
|
+
outcome = `This ${decision} was not applied. ${corruptStoredRowContent(where)}`;
|
|
335
|
+
}
|
|
329
336
|
else if (typeof status === 'number') {
|
|
330
337
|
const msg = e instanceof Error ? e.message : String(e);
|
|
331
338
|
hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
|
|
@@ -7,6 +7,7 @@ import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded
|
|
|
7
7
|
import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
|
|
8
8
|
import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
|
|
9
9
|
import { denyAttributionRefusalFromError } from '../wireRefusalCopy.js';
|
|
10
|
+
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
10
11
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
11
12
|
import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS, } from '@sema-agent/sdk';
|
|
12
13
|
export function isFsApprovalGate(gate) {
|
|
@@ -28,6 +29,10 @@ export function isToolApprovalGate(gate) {
|
|
|
28
29
|
return true;
|
|
29
30
|
return typeof gate?.toolName === 'string' && toolNameIsShellExec(gate.toolName);
|
|
30
31
|
}
|
|
32
|
+
function withCorruptStoredRow(reason, err) {
|
|
33
|
+
const where = corruptStoredRowWhere(err);
|
|
34
|
+
return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
|
|
35
|
+
}
|
|
31
36
|
export function structurallyEqual(a, b) {
|
|
32
37
|
if (a === b)
|
|
33
38
|
return true;
|
|
@@ -199,7 +204,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
199
204
|
kind: 'failed',
|
|
200
205
|
stage: 'decide',
|
|
201
206
|
gatedCallId,
|
|
202
|
-
reason: `decide(approve) failed: ${String(e)}`,
|
|
207
|
+
reason: withCorruptStoredRow(`decide(approve) failed: ${String(e)}`, e),
|
|
203
208
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
204
209
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
205
210
|
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
@@ -256,7 +261,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
256
261
|
kind: 'failed',
|
|
257
262
|
stage: 'decide',
|
|
258
263
|
gatedCallId,
|
|
259
|
-
reason: `decide(deny) failed: ${String(e)}`,
|
|
264
|
+
reason: withCorruptStoredRow(`decide(deny) failed: ${String(e)}`, e),
|
|
260
265
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
261
266
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
262
267
|
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export * from './promptProfileWireCaps.js';
|
|
|
77
77
|
export * from './retainBackgroundWireCaps.js';
|
|
78
78
|
export * from './oneShotWireCaps.js';
|
|
79
79
|
export * from './rewindWireCaps.js';
|
|
80
|
+
export * from './rewindArchiveCapability.js';
|
|
81
|
+
export * from './registryQuotaUsage.js';
|
|
80
82
|
export * from './selfOrchestrationWireCaps.js';
|
|
81
83
|
export * from './skillsWireCaps.js';
|
|
82
84
|
export * from './ultracodeWireCaps.js';
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,8 @@ export * from './promptProfileWireCaps.js';
|
|
|
76
76
|
export * from './retainBackgroundWireCaps.js';
|
|
77
77
|
export * from './oneShotWireCaps.js';
|
|
78
78
|
export * from './rewindWireCaps.js';
|
|
79
|
+
export * from './rewindArchiveCapability.js';
|
|
80
|
+
export * from './registryQuotaUsage.js';
|
|
79
81
|
export * from './selfOrchestrationWireCaps.js';
|
|
80
82
|
export * from './skillsWireCaps.js';
|
|
81
83
|
export * from './ultracodeWireCaps.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type QuotaAmount = {
|
|
2
|
+
kind: 'value';
|
|
3
|
+
value: number;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'unlimited';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'unknown';
|
|
8
|
+
};
|
|
9
|
+
export type QuotaResetAt = {
|
|
10
|
+
kind: 'at';
|
|
11
|
+
iso: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'not-exhausted';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'unknown';
|
|
16
|
+
};
|
|
17
|
+
export interface QuotaWindowReading {
|
|
18
|
+
readonly window: 'fiveHour' | 'weekly';
|
|
19
|
+
readonly limit: QuotaAmount;
|
|
20
|
+
readonly used: QuotaAmount;
|
|
21
|
+
readonly remaining: QuotaAmount;
|
|
22
|
+
readonly exhausted: boolean | undefined;
|
|
23
|
+
readonly resetAt: QuotaResetAt;
|
|
24
|
+
readonly isEstimate: boolean | undefined;
|
|
25
|
+
}
|
|
26
|
+
export type QuotaConfigured = 'configured' | 'not-configured' | 'unknown';
|
|
27
|
+
export interface QuotaUsageReading {
|
|
28
|
+
readonly principal: string | undefined;
|
|
29
|
+
readonly configured: QuotaConfigured;
|
|
30
|
+
readonly source: string | undefined;
|
|
31
|
+
readonly fiveHour: QuotaWindowReading;
|
|
32
|
+
readonly weekly: QuotaWindowReading;
|
|
33
|
+
readonly lastResetAt: {
|
|
34
|
+
kind: 'at';
|
|
35
|
+
iso: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'never';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'unknown';
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export declare function projectRegistryQuotaUsage(body: unknown): QuotaUsageReading | undefined;
|
|
43
|
+
export declare function quotaAmountText(amount: QuotaAmount): string;
|
|
44
|
+
export declare function registryQuotaDoctorDetail(reading: QuotaUsageReading | undefined): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
2
|
+
function amountOf(holder, key) {
|
|
3
|
+
if (!Object.hasOwn(holder, key))
|
|
4
|
+
return { kind: 'unknown' };
|
|
5
|
+
const v = holder[key];
|
|
6
|
+
if (v === null)
|
|
7
|
+
return { kind: 'unlimited' };
|
|
8
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
9
|
+
return { kind: 'value', value: v };
|
|
10
|
+
return { kind: 'unknown' };
|
|
11
|
+
}
|
|
12
|
+
function usedOf(holder) {
|
|
13
|
+
const v = holder['used'];
|
|
14
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
15
|
+
return { kind: 'value', value: v };
|
|
16
|
+
return { kind: 'unknown' };
|
|
17
|
+
}
|
|
18
|
+
function boolOf(holder, key) {
|
|
19
|
+
const v = holder[key];
|
|
20
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
21
|
+
}
|
|
22
|
+
function resetAtOf(holder, exhausted) {
|
|
23
|
+
if (!Object.hasOwn(holder, 'resetAt'))
|
|
24
|
+
return { kind: 'unknown' };
|
|
25
|
+
const v = holder['resetAt'];
|
|
26
|
+
if (typeof v === 'string' && v !== '')
|
|
27
|
+
return { kind: 'at', iso: v };
|
|
28
|
+
if (v === null)
|
|
29
|
+
return exhausted === false ? { kind: 'not-exhausted' } : { kind: 'unknown' };
|
|
30
|
+
return { kind: 'unknown' };
|
|
31
|
+
}
|
|
32
|
+
function windowOf(body, window) {
|
|
33
|
+
const raw = body[window];
|
|
34
|
+
if (!isObj(raw)) {
|
|
35
|
+
return {
|
|
36
|
+
window,
|
|
37
|
+
limit: { kind: 'unknown' },
|
|
38
|
+
used: { kind: 'unknown' },
|
|
39
|
+
remaining: { kind: 'unknown' },
|
|
40
|
+
exhausted: undefined,
|
|
41
|
+
resetAt: { kind: 'unknown' },
|
|
42
|
+
isEstimate: undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const exhausted = boolOf(raw, 'exhausted');
|
|
46
|
+
return {
|
|
47
|
+
window,
|
|
48
|
+
limit: amountOf(raw, 'limit'),
|
|
49
|
+
used: usedOf(raw),
|
|
50
|
+
remaining: amountOf(raw, 'remaining'),
|
|
51
|
+
exhausted,
|
|
52
|
+
resetAt: resetAtOf(raw, exhausted),
|
|
53
|
+
isEstimate: boolOf(raw, 'isEstimate'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function projectRegistryQuotaUsage(body) {
|
|
57
|
+
if (!isObj(body))
|
|
58
|
+
return undefined;
|
|
59
|
+
const principal = typeof body['principal'] === 'string' ? body['principal'] : undefined;
|
|
60
|
+
const tq = body['tokenQuota'];
|
|
61
|
+
const configured = !Object.hasOwn(body, 'tokenQuota')
|
|
62
|
+
? 'unknown'
|
|
63
|
+
: tq === null
|
|
64
|
+
? 'not-configured'
|
|
65
|
+
: isObj(tq)
|
|
66
|
+
? 'configured'
|
|
67
|
+
: 'unknown';
|
|
68
|
+
const source = isObj(tq) && typeof tq['source'] === 'string' ? tq['source'] : undefined;
|
|
69
|
+
const lastRaw = body['lastResetAt'];
|
|
70
|
+
const lastResetAt = !Object.hasOwn(body, 'lastResetAt')
|
|
71
|
+
? { kind: 'unknown' }
|
|
72
|
+
: lastRaw === null
|
|
73
|
+
? { kind: 'never' }
|
|
74
|
+
: typeof lastRaw === 'string' && lastRaw !== ''
|
|
75
|
+
? { kind: 'at', iso: lastRaw }
|
|
76
|
+
: { kind: 'unknown' };
|
|
77
|
+
return {
|
|
78
|
+
principal,
|
|
79
|
+
configured,
|
|
80
|
+
source,
|
|
81
|
+
fiveHour: windowOf(body, 'fiveHour'),
|
|
82
|
+
weekly: windowOf(body, 'weekly'),
|
|
83
|
+
lastResetAt,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function quotaAmountText(amount) {
|
|
87
|
+
switch (amount.kind) {
|
|
88
|
+
case 'value':
|
|
89
|
+
return String(amount.value);
|
|
90
|
+
case 'unlimited':
|
|
91
|
+
return 'no cap';
|
|
92
|
+
case 'unknown':
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function registryQuotaDoctorDetail(reading) {
|
|
97
|
+
if (reading === undefined) {
|
|
98
|
+
return 'token quota not readable — the quota response could not be read as an object, so nothing is known about this principal (this is not the same as "no quota configured")';
|
|
99
|
+
}
|
|
100
|
+
switch (reading.configured) {
|
|
101
|
+
case 'not-configured':
|
|
102
|
+
return 'no token quota configured for this principal on this instance — requests are not capped by a token quota here, so a missing number is the truth rather than a zero';
|
|
103
|
+
case 'configured': {
|
|
104
|
+
const five = quotaAmountText(reading.fiveHour.remaining);
|
|
105
|
+
const week = quotaAmountText(reading.weekly.remaining);
|
|
106
|
+
return `token quota configured${reading.source === undefined ? '' : ` by ${reading.source}`} — remaining this five-hour window: ${five}; remaining this week: ${week} (estimates)`;
|
|
107
|
+
}
|
|
108
|
+
case 'unknown':
|
|
109
|
+
return 'token quota not reported in a readable shape — the response carried no readable quota field, so whether this principal has a quota is unknown; do not render it as "none"';
|
|
110
|
+
}
|
|
111
|
+
}
|