@sema-agent/client-core 0.62.2 → 0.63.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 +180 -0
- package/README.md +9 -2
- package/dist/autoModeUnavailable.d.ts +77 -0
- package/dist/autoModeUnavailable.js +101 -0
- package/dist/engineIdentity.d.ts +94 -0
- package/dist/engineIdentity.js +143 -0
- package/dist/engineNoticeCodes.d.ts +91 -0
- package/dist/engineNoticeCodes.js +215 -0
- package/dist/gateVocabulary.d.ts +67 -0
- package/dist/gateVocabulary.js +134 -0
- package/dist/hitl/persistedRulesWire.d.ts +63 -1
- package/dist/hitl/persistedRulesWire.js +86 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/liveInitToolFace.js +13 -0
- package/dist/permissionRuleIssue.d.ts +33 -0
- package/dist/permissionRuleIssue.js +126 -0
- package/dist/postureKnob.d.ts +92 -0
- package/dist/postureKnob.js +181 -0
- package/dist/steering.d.ts +6 -1
- package/dist/steering.js +10 -2
- package/dist/subagentContentStore.d.ts +5 -1
- package/dist/subagentContentStore.js +4 -2
- package/dist/toolRoster.d.ts +182 -0
- package/dist/toolRoster.js +236 -0
- package/dist/workflow.js +4 -1
- package/dist/workflowClient.js +63 -6
- package/dist/workflowMonitor.d.ts +20 -4
- package/dist/workflowMonitor.js +10 -0
- package/docs/INTEGRATION-CLIENTS.md +526 -9
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,186 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.63.1(2026-09-09)
|
|
53
|
+
|
|
54
|
+
test 对 0.63.0 的黑盒回执(G-1…G-20 五路交叉)提了六项;逐项亲核后 **四修二订**。本段只记内容,
|
|
55
|
+
不含任何 bump。
|
|
56
|
+
|
|
57
|
+
### 修复
|
|
58
|
+
|
|
59
|
+
- 🔴 **workflow 监视器把「停在审批门上」渲成「在跑」**(G-20)。engine core 7.10.0 起一条 workflow
|
|
60
|
+
**agent 腿**可以耐久挂在审批卡上(`WorkflowItemStatus` +`parked`,`deriveAgentDisplayStatus` 直接产
|
|
61
|
+
`"parked"`),而本包监视器车道的窄读把它折进了开集兜底 —— 屏上那一行说的是「等它跑完」,实际它在
|
|
62
|
+
有人做决定之前**永远不会**自己往前走一步。修:四处窄读各给 `parked` 一条自己的臂
|
|
63
|
+
(`coerceAgentState` / `agentDisplayStatus` / `coerceRunStatus` / `coercePhaseStatus` + 合成格三元式),
|
|
64
|
+
开集兜底一个字节不动。🔴 **`agentDisplayStatus` 的这条臂必须排在 `!workflowActive` 之前** —— park
|
|
65
|
+
之后整只 run 收在 `failed`,排在后面就永远被 `interrupted` 抢答。
|
|
66
|
+
型面(additive,端的穷尽形要各加一条 case):`AgentState` / `DisplayStatus` /
|
|
67
|
+
`WorkflowRunState['status']` / `WorkflowPhase['status']` 各 +`parked`。详见 §27h3、换装清单 §27i ⑨。
|
|
68
|
+
|
|
69
|
+
- **同一条 wire 的同一个 `status`,三处在场判不同形**(G-20 附带②)。`parseWorkflowPollEnvelope` 判
|
|
70
|
+
`length > 0`(空串=缺席),而 `projectWorkflowTaskOutput` 的 per-agent 与 `projectAgent` 的
|
|
71
|
+
`displayStatus` 只判 `typeof`(空串=在场)。后果:前者把空串渲成 `[]` —— 一对什么也没说的方括号,
|
|
72
|
+
却长得像一个真答案;后者更坏 —— `'' ?? row.status` 恒取空串(`??` 只挡 null/undefined),那条腿的
|
|
73
|
+
raw `status` 被**静默丢掉**,连回落都走不到(`parked` 正是从这条缝里漏的)。三处统一成
|
|
74
|
+
**空串 = 缺席**;纯空白仍不 trim(两侧同尺)。
|
|
75
|
+
|
|
76
|
+
### 文档 / 门
|
|
77
|
+
- **异源对抗复审 R2 三条 [medium] 全采纳**(仍全部打在 E 段这条 G-8 承重腿上,三条都真能复现):
|
|
78
|
+
①`underAnyBranch` 漏了**循环** —— 零次循环里的铸点被认作「无条件」⇒ 补 `for`/`for-of`/`for-in`/
|
|
79
|
+
`while`/`do` 五形;②**铸点与 `/health` 之间的接线没验** —— 把引擎 deployment 对象里的
|
|
80
|
+
`instanceId,` 删掉,两条断言照样绿而实际 `deps.instanceId` 已是 `undefined`、条件 spread 会把这一位
|
|
81
|
+
整只省掉 ⇒ 新增 E3 接线断言(铸出的值必须真被挂到一只对象上);③**条件 spread 被当成必然输出** ——
|
|
82
|
+
守卫换成别的旗子、或经包装调用摊进来,都被判成「带上了」⇒ 改成只认「守卫就是那个值本身」的形,
|
|
83
|
+
包装调用一律判不出(返回形状是运行期的事,不替它编答案)。
|
|
84
|
+
同批把射程边界写进段头与 §27d:**证的是源码形**(挡上游改锚/改铸法/断接线),**不证**「这段码真被
|
|
85
|
+
执行」——那要一次真启动的 `/health` 响应,不在本包射程内(本包不起引擎)。
|
|
86
|
+
- **自查同批补两条**(不在复审名单里,同一条病形):①`hasProp` 原是整棵子树遍历 ⇒ `restart: {…}`
|
|
87
|
+
这类**嵌套值**里的同名键会被算进 `/health` 的键空间,改成「直挂 + 沿 spread 下行」;
|
|
88
|
+
②①c 的 `statedTableSize` 取第一处命中 ⇒ 同名两处各写一个数时放过另一处,改成**恰一处**才作数;
|
|
89
|
+
并加「档里每一处带表长的签名行都必须已登记」的漏登即红(新增一张带数字的表时门不再沉默)。
|
|
90
|
+
|
|
91
|
+
- **异源对抗复审 R1 三条 [medium] 全采纳**(都打在本批**新加的门**上,三条都是假绿):
|
|
92
|
+
①`run-engine-identity-test.mjs` E 段原是两条裸正则 —— 条件里铸、注释残留、挂在别的对象上三类反例
|
|
93
|
+
都骗得过去 ⇒ 改锚 AST(控制流 + 路由两轴),并把三类反例做成常驻反向正控;
|
|
94
|
+
②同段见证缺席时仍报「全绿」⇒ 改成未注入打 `SKIPPED-SECTION`(汇总显形 PARTIAL)、注入了却拿不到
|
|
95
|
+
物料**判红**,结语逐字说清这一段跑没跑;
|
|
96
|
+
③`run-integration-doc-freshness-test.mjs` ①c 把 AST 节点数当运行期表长 —— 展开元素算一员、任意
|
|
97
|
+
单参调用都收 ⇒ 改成逐条 fail-closed(只认 `Object.freeze(<全字符串字面量数组>)`),复审给的原样
|
|
98
|
+
反例(把一员换成展开)现在当场判工具红。
|
|
99
|
+
|
|
100
|
+
- **`/health` 的 `pid` 单锚碰撞**(G-8):裁读 = **设计不改,判据收窄 + 取证入门**。「只剩 `pid` 可比
|
|
101
|
+
且相等」要求 `instanceId` 在两侧都缺席,而 `instanceId` 在引擎里是**无条件铸**的、`/health` 一路
|
|
102
|
+
带着它 ⇒ 那种读数在生产上到不了;反过来把它判成 `unknown`,会让一台只报得出 `pid` 的老 worker
|
|
103
|
+
**每一次心跳**都触发重握手 —— 那是拿一个每拍都发生的代价去换一个到不了的风险。
|
|
104
|
+
落点:§27d 加一段写清「pid 是最弱锚、从不落单」+ 端的兜底句;`run-engine-identity-test.mjs` 加
|
|
105
|
+
**D 段**(设计钉:pid 单锚相等仍 same / pid 相等压不住更强的锚 / 优先序里 pid 是表尾)与
|
|
106
|
+
**E 段**(引擎产物第二见证,**锚 AST 不锚子串**:铸点不在任何 `if`/`switch`/三元/`??` 里,且
|
|
107
|
+
`instanceId` 挂在 `/health` **那一只**响应体上 —— 按 `status:"ok"` + `pid: process.pid` 两键定位。
|
|
108
|
+
🔴 `SEMA_FENCE_ENGINE_DIR` 未注入 ⇒ 打 `SKIPPED-SECTION`(汇总显形 PARTIAL,不冒充全绿);
|
|
109
|
+
注入了却拿不到物料 ⇒ **判红**。发布批必须注入一次引擎树把这一段跑绿,才能引用本条的结论)。
|
|
110
|
+
|
|
111
|
+
- **§27h 的 `engine_notice` 码册数字滞后**(G-16):档写「四十八码」,源码那张表(与它的头注)早就是
|
|
112
|
+
**50** 员 —— 上游加了两个码,档没跟。修:数字订正,**并把它变成有牙的** —— `run-integration-doc-
|
|
113
|
+
freshness-test.mjs` 新增 **①c 段**:签名块里的表长数字 ⟷ 源码那张表的真实成员数(TS AST 取
|
|
114
|
+
`Object.freeze([...])` 的元素个数)。同批把同形的五张表全部登记进去(`ENGINE_NOTICE_CODES` /
|
|
115
|
+
`MCP_INJECTION_DROP_REASONS` / `GATE_DENIED_BY_WORDS` / `ASK_ORIGIN_WORDS` /
|
|
116
|
+
`PERMISSION_RULE_ISSUE_CODES`),并把这几处的中文数词改成阿拉伯数字 —— 读不出来的门等于没有门,
|
|
117
|
+
所以抽取器读不出时判**工具红**而不是静默放过。
|
|
118
|
+
|
|
119
|
+
- 🔴 **表外 lint 码被 60 字符封长,与文档「原样带上」冲突**(G-11)。码是**引擎产的标识符**不是散文 ——
|
|
120
|
+
运维要拿它整只去问上游,截一半的码谁也查不出来,还长得像一个完整答案。修:码**原样带上不封长**,
|
|
121
|
+
呈前只走本包同一只不可见字符消毒器(不封长 ≠ 不消毒);`message` 半场的封长照旧(那半场确实是
|
|
122
|
+
散文)。同族其余三处 `capForDisplay` 的词位(posture / read-face / write-protection 的来源词)
|
|
123
|
+
刻意不动:那些是**有界词表**里的词、且文档从未承诺「原样」,与本条不同形。详见 §27f、§27i ⑤。
|
|
124
|
+
|
|
125
|
+
- 🔴 **`postureKnobDetail` 的判序反了**(G-3)。旧序先看「读数在不在」再看 `reachable` ⇒ 端拿着上一拍
|
|
126
|
+
的 view、这一拍 operator 探测打不通、只翻 `reachable:false` 时,屏上照渲**上一拍的值和来源词**,
|
|
127
|
+
一个字都看不出这一次根本没读到 —— 拿陈读数冒充现势。修:**`!reachable` 先判**,压过任何在手的
|
|
128
|
+
读数(打不通时旧读数不是现势),那一句仍是既有的「未观测」原文,值与来源词一个都不漏。
|
|
129
|
+
端因此**不必**再为打不通而先把 view 清空。详见 §27b、换装清单 §27i ①。
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
- **§27h3 整节重写**:0.63.0 原文两处失真留痕订正 —— ①「core 7.10.0 给 **run** 状态加了 parked」错,
|
|
133
|
+
加的是 **agent 行**那一级(run 词表今天仍闭在 running|completed|failed);②「本包零改」只对
|
|
134
|
+
TaskOutput 文本车道成立,监视器投影车道当时正在折它。同节把 `readWorkflowRunLine`(本包零命中)
|
|
135
|
+
订正成真名 `parseWorkflowPollEnvelope`。§27j 的 G-20 判据同批订正口径(park 落在腿上)。
|
|
136
|
+
|
|
137
|
+
## 0.63.0(2026-09-09)
|
|
138
|
+
|
|
139
|
+
对表 **sdk 8.8.0 / engine ≥7.67.0 / core 7.10.0**(devDep core `~7.3.0` → `~7.10.0`)。sdk 地板 `>=8.5.0` → `>=8.8.0`
|
|
140
|
+
(peerDependencies / devDependencies / README 地板句 / `run-sdk-floor-test.mjs` 的 `FLOOR` 四处同批抬齐)。
|
|
141
|
+
|
|
142
|
+
### BREAKING 迁移表(消费端要改的点)
|
|
143
|
+
|
|
144
|
+
| 迁移前 | 迁移后 | 为什么 |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `serverGates.durableApproval`(裸布尔) | `serverGates.durableApproval.value`,来源读 `postureKnobSourceOf()` | 一个裸值答不出「这台机器为什么是这个值、怎么钉回」;单机 turnkey 上两根旋钮的缺席默认已翻转,不显形就没人看得见 |
|
|
147
|
+
| `rules.revoke({rule, scope})` 两元组 | `revokeTargetFromPersistedRule(row)`(铸 `{behavior, rule, scope}`) | deny/ask 也进同一个规则店后,同文本的 `deny` 与 `allow` 是**两条不同的行**;猜一态会不可逆地删掉另一态,而调用方收到 200 |
|
|
148
|
+
|
|
149
|
+
### 新增
|
|
150
|
+
|
|
151
|
+
- **`src/postureKnob.ts`** —— operator 面 `serverGates` 三根 posture 旋钮的读数窄读器:
|
|
152
|
+
`projectServerGateKnobs()` 三键窄读(老 worker 的裸值折进同一只壳并立 `legacy` 位,另两根整键缺席
|
|
153
|
+
时**诚实缺席、不铸默认**;主锚坏 ⇒ 整段缺席,旁枝坏 ⇒ 只丢那一根)、`postureKnobSourceOf()`
|
|
154
|
+
(legacy 行答 `undefined` —— 那台 worker 没说过是谁定的)、`postureSourceIsOperatorPinned()`
|
|
155
|
+
(只有 `env`/`center` 算运维表过态)、`postureKnobDetail()` 四句逐字互异的唯一措辞铸点(毫秒渲人话)。
|
|
156
|
+
来源四词表 `POSTURE_SOURCE_WORDS` 在本包**只有这一份**,与 `readFacePosture` 共用。
|
|
157
|
+
门:`scripts/run-posture-knob-projection-test.mjs`。
|
|
158
|
+
|
|
159
|
+
- **`hitl/persistedRulesWire.ts` 三态规则身份** —— `PERSISTED_RULE_BEHAVIORS`(sdk `RuleBehavior` 的
|
|
160
|
+
运行期镜像,逐词对账)、`persistedRuleBehaviorOf()`(闭三词之外含缺席一律 `undefined`,**不补默认**)、
|
|
161
|
+
`persistedRuleBehaviorLabel()` / `PERSISTED_RULE_BEHAVIOR_UNKNOWN`(表外词原样渲、缺席渲 `(unknown)`,
|
|
162
|
+
不冒充三态)、`revokeTargetFromPersistedRule()`(身份三元组逐字回传 + 可选 `principal`;**`behavior`
|
|
163
|
+
读不出即拒铸**)。`listAllPersistedRules` 行为不变:缺 `behavior` 的行**仍进治理清单**(丢行 =
|
|
164
|
+
把一条活规则藏起来),态那一列渲 `(unknown)`、撤销入口藏起来。
|
|
165
|
+
门:`scripts/run-rules-side-test.mjs` 新增 G9 段(38 checks)。
|
|
166
|
+
|
|
167
|
+
- **`src/engineIdentity.ts`** —— `/health` 三只**代际锚**(`pid` / `instanceId` / `startedAt`)的窄读器
|
|
168
|
+
与纯比较。`/health` 是唯一免凭证的门、心跳恒绿,`startedAt`(server ≥7.67.0 无条件发)把「这条端口
|
|
169
|
+
现在服务的是另一条命」从「等一次 401」变成一等事实。判词是**三态**(`changed` / `same` /
|
|
170
|
+
`unknown`)不是布尔:锚**交集为空** ⇒ `unknown`,绝不折成 `same`;**任一**可比锚不同即 `changed`
|
|
171
|
+
(不让 `startedAt` 独裁);布尔口 `engineIdentityChanged()` 只有 `true` 是一句断言。
|
|
172
|
+
「换代之后做什么」是宿主的状态机,不在本包。
|
|
173
|
+
门:`scripts/run-engine-identity-test.mjs`(77 checks)。
|
|
174
|
+
|
|
175
|
+
- **`src/gateVocabulary.ts`** —— 门词汇两张表的唯一措辞铸点:`GATE_DENIED_BY_WORDS`(**九词**,
|
|
176
|
+
core 7.9.0 加 `persisted_rule`)+ `gateDeniedByDetail()`;`ASK_ORIGIN_WORDS`(**十一词**,
|
|
177
|
+
core 7.9.0 加 `rule_store_unavailable`)+ `askOriginDetail()`。两张表逐词逐序抄自 sdk 联合声明,
|
|
178
|
+
门从实装 `.d.ts` 解出成员**双向等值**对账。🔴 两张表的开闭**各按其出处**,兜底句因此逐字分家:
|
|
179
|
+
`DeniedBy` 在 wire 上是真闭集(出集记录被引擎整条 withhold)⇒ 表外词 = **坏记录**;`AskOrigin`
|
|
180
|
+
是真开集(server 只判非空串)⇒ 表外词 = **这个词比这一端新**,照常回答那只 ask。
|
|
181
|
+
门:`scripts/run-gate-vocabulary-test.mjs`(85 checks)。
|
|
182
|
+
|
|
183
|
+
- **`src/permissionRuleIssue.ts`** —— 权限规则 lint 的拒因码表(`PERMISSION_RULE_ISSUE_CODES`,十五码)
|
|
184
|
+
+ 唯一措辞铸点 `permissionRuleIssueDetail(code, message)`。sdk 8.8.0 **既无 spec schema 也无导出型**
|
|
185
|
+
(sdk README 自己登记),故按引擎真字节铸;头注写清**候上游导出即整只退役改成从 sdk 取**,并给下游
|
|
186
|
+
留了反漂移锚(`dts-pattern` 盯 sdk `PermissionRuleIssue` 上的 `code` 声明)。
|
|
187
|
+
措辞按前缀两族分家:`invalid.*` = 规则写错了(改规则)/ `unsupported.*` = 规则合法但这条车道不接
|
|
188
|
+
(换写法或换个面落),两族逐字互斥。引擎那句 `message` 是**散文**:带上、呈前消毒 + 转义后封长,
|
|
189
|
+
绝不当码用。另设 `RETIRED_PERMISSION_RULE_ISSUE_CODES` 记账座显形 core 声明里那一员改名遗留。
|
|
190
|
+
门:`scripts/run-permission-rule-issue-codes-test.mjs`(92 checks;注入 `SEMA_FENCE_ENGINE_DIR` 后 94)。
|
|
191
|
+
|
|
192
|
+
- **`src/toolRoster.ts`** —— 一条腿的**工具名册**(`wiring_manifest.tools`)投影 +
|
|
193
|
+
`tool_roster_delta` 应用。`projectToolRoster()` / `toolRosterNames()` / `toolShimFromRoster()` /
|
|
194
|
+
`applyToolRosterDelta()`。🔴 照抄引擎自己的**绝不半张名册**纪律:任一行读不出、或 `count` 与真实
|
|
195
|
+
行数对不上 ⇒ **整只判没**(交一份少了行的名册,消费端会把读不出的行当成「这只工具没挂」);
|
|
196
|
+
例外只有**面**(`pathTarget` / `renderHints` 坏 ⇒ 只丢那一格,行还在)。`pathTarget` 的
|
|
197
|
+
`base` / `absent` / `patternParam`(sdk 8.7.0 / core 7.9.1 #635)可选放行、缺席不铸默认。
|
|
198
|
+
🔴 `fromDigest` 对不上**不是拒绝**(契约里唯一的硬话):名册照换、只丢 `summary` + 立 `skew` 位 ——
|
|
199
|
+
拒绝换会让消费端永远抱着一份过期名册。
|
|
200
|
+
`liveInitToolFace.ts` 的三张 tap 实测估计词表**本批不删**(名册只在 effective 半场、且 ≥7.9.0 才真推,
|
|
201
|
+
回落还得留着),头注登记退役条款:**名册恒在场的引擎版本成为支持底线即删表**。
|
|
202
|
+
门:`scripts/run-tool-roster-projection-test.mjs`(127 checks)。
|
|
203
|
+
|
|
204
|
+
- **`src/engineNoticeCodes.ts`** —— `engine_notice` 的**码册**(`ENGINE_NOTICE_CODES`,core 7.9.x 的
|
|
205
|
+
四十八码逐字镜像,含本批加员 `mcp.injection_dropped`)与 **audience 表**
|
|
206
|
+
(`ENGINE_NOTICE_AUDIENCE` / `noticeAudienceOf()`,含 `mcp.injection_dropped` 的 **user** 行)+
|
|
207
|
+
`engineNoticeInCatalog()` + `MCP_INJECTION_DROP_REASONS` + `readMcpInjectionDrop()`。
|
|
208
|
+
「渲不渲」的判据是**上游有没有把这个码铸进成文码册**,不是这一端有没有那一格文案;audience 答的是
|
|
209
|
+
另一问(说给谁听),表外码**保守判 operator**。在册判据**认原始值** —— 消毒只进文案,不进判据。
|
|
210
|
+
🔴 **单铸律**:上游对 `mcp.injection_dropped` 立了 `settlement.single_mint`(宿主供事实、core 铸句子),
|
|
211
|
+
所以本包**只给事实窄读器、一句措辞都不抄**,转录直接用引擎给的 `message`;门里有反向钉守着。
|
|
212
|
+
🆕 **core 7.10.0 加员 +2**:`delegation.ask_unresolvable`(audience **user** —— 委派链上那只 ask
|
|
213
|
+
判不出归属、没有人可问,收件人是发起委派的用户)/ `config.read_face_swapped`(audience **operator**
|
|
214
|
+
—— READ 容纳面被换过档,是部署事实)。码册 48 ⇒ **50**,audience 表同批。
|
|
215
|
+
门:`scripts/run-engine-notice-catalog-test.mjs`(163 checks)。
|
|
216
|
+
|
|
217
|
+
- **`src/autoModeUnavailable.ts`**(件⑧;core 7.10.0 #616)—— 「这只 ask 是因为**分类器跑不了**才问人」
|
|
218
|
+
的事实读器 + 唯一措辞铸点:`AUTO_MODE_UNAVAILABLE_CAUSES` / `AUTO_MODE_BREAKER_CAUSES` 两张镜像表、
|
|
219
|
+
`classifierUnavailableOf()`(**一只读器吃两处** —— `AskRequest.classifierUnavailable` 与 durable park 行的
|
|
220
|
+
`PendingAction.tool_approval.classifierUnavailable` 键路同形)、`classifierUnavailableDetail()`。
|
|
221
|
+
🔴 **两条 cause 轴刻意不合并**:一条答「这一轮分类为什么没跑成」、一条答「熔断闩为什么合上」,
|
|
222
|
+
交于 `error`/`timeout`、各有一个独占成员。🔴 **`parse_error` 只在熔断轴上** —— 上游顶注逐字
|
|
223
|
+
「stamps nothing」:分类器**跑了并且答了**,只是答在契约之外;读器按不可用轴收窄,读到它判**缺席**,
|
|
224
|
+
把它当成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
225
|
+
🔴 它是**显示元数据不是裁决位**;**缺席 ≠「分类器跑成了」**(绝大多数 ask 根本没咨询过分类器)。
|
|
226
|
+
门:`scripts/run-auto-mode-unavailable-test.mjs`(92 checks)。
|
|
227
|
+
|
|
228
|
+
- **`WorkflowRunStatus` 的 `parked`:零改**。`src/workflow.ts` 两处读法本来就把 `status` 当**开集
|
|
229
|
+
字符串**原样透出,从不窄读成枚举 ⇒ 新词自动逐字到达端上。详见 §27h3(同处如实登记了一条**先于
|
|
230
|
+
本批存在**的已知形:`status` 读不出时回落 `'completed'`)。
|
|
231
|
+
|
|
52
232
|
## 0.62.2(2026-09-09)
|
|
53
233
|
|
|
54
234
|
> patch 内容批(一件)。**本段只记内容**:`package.json.version` 未动、README `Version` 行未动、
|
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.63.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
|
|
@@ -67,7 +67,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
67
67
|
against — the tables live upstream precisely so this package does not keep a second copy that can
|
|
68
68
|
fall behind. The browser bundle really bundles the SDK through (the portability guard would
|
|
69
69
|
exit 3 rather than quietly mark it external).
|
|
70
|
-
- The declared floor is `>=8.
|
|
70
|
+
- The declared floor is `>=8.8.0`, and it is *witnessed*: the guard checks that an actually
|
|
71
71
|
installed SDK at that line still exports every value-level symbol this package imports and still
|
|
72
72
|
declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
|
|
73
73
|
promise, not a contract.
|
|
@@ -237,6 +237,13 @@ public-surface guard checks that last one).
|
|
|
237
237
|
| `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code |
|
|
238
238
|
| `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but carries no such key (an older engine), the engine explicitly answered `null` — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock |
|
|
239
239
|
| `scripts/run-terminal-cause-projection-test.mjs` | The `7.64.0` wire reshape, projected. A run's ending stopped being eight parallel flat keys and became **one tagged cause** (`completed | failed | blocked | paused`), and a tool call's gate stopped being four orthogonal words and became **one record** (`disposition` / `settlement?` / `origin?`). Both are read in exactly one place in this package, and this guard pins them at **two levels**, because the dangerous seam is "the reader was updated, the consumer was not": each terminal arm is checked on the reader *and* on the `subtype` / `is_error` / `errors[]` the projector actually emits. Two properties carry most of the weight. First, a terminal word this reader does not know is **never** laundered into an empty success — it lands on an `unknown` arm carrying the word verbatim, while a payload with no terminal word at all (the mock lane) keeps the success arm exactly as before, which is the one and only case the reader answers `null`. Second, the three window words (`approval_window_expired`, `denial_limit_window_expired`, `park_sla_expired`) must each be told apart by a different predicate: the previous generation collapsed all three onto one `timeout`, and re-merging them would throw away the discrimination this reshape just restored. Two byte generations are read by one reader, keyed on the discriminator upstream nailed (`"terminal" in result`): the current cause form, and the **flat** form that a current engine still emits on two lanes — replayed persisted bytes, which the service passes through verbatim rather than back-filling, and the service's own rejection envelope. A cause-form payload that also carries stale flat keys must ignore them entirely: keeping one compatibility read is what gives a single fact two sources. The same file also pins the MCP delivery verdict and HTTP status riding the wiring manifest, the four-state write-protection reading (where three of the four states mean *cannot tell*, and none of them may be printed as "there is no table"), and the park-reopen fetch identity: that predicate is asserted through the **real entry point**, since the defect being fixed was precisely a call site wired to a different predicate than the one that routed the row there |
|
|
240
|
+
| `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. Two cause axes exist upstream and the guard pins them apart: why one classification round failed, and why the session breaker latched. They overlap on two words and each owns a third, so folding them into one table would throw the distinction away — and one of those third words, `parse_error`, is stamped nowhere near this fact, because the classifier did run and did answer, just outside its contract. The reader therefore narrows on the availability axis and treats that word as absence rather than inventing a failure the engine says did not happen. 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 reconciled against the engine's own bytes |
|
|
241
|
+
| `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails |
|
|
242
|
+
| `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever |
|
|
243
|
+
| `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 |
|
|
244
|
+
| `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) |
|
|
245
|
+
| `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 |
|
|
246
|
+
| `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 |
|
|
240
247
|
| `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 |
|
|
241
248
|
| `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 |
|
|
242
249
|
| `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 |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/autoModeUnavailable.ts — 「这只 ask 是因为**分类器跑不了**才问人」的事实读器 + 唯一措辞铸点
|
|
3
|
+
* (0.63.0 件⑧;core 7.10.0 #616)。
|
|
4
|
+
*
|
|
5
|
+
* -- 它答的是哪一问 ---------------------------------------------------------------------------
|
|
6
|
+
* auto 模式下,门会就一只 ask 去咨询分类器。分类器**没跑成**时,引擎在这只 ask 上盖一格
|
|
7
|
+
* `classifierUnavailable: { cause }` —— 卡上那句「问你是因为分类器这次跑不了(超时)」的事实来源。
|
|
8
|
+
* 🔴 **它是显示元数据,不是裁决位**:没有任何东西读它来决定放行与否(引擎在门上已经判完了),
|
|
9
|
+
* 所以消费端只拿它渲一句话,**绝不**据它翻转任何裁决。
|
|
10
|
+
* 🔴 **缺席 ≠「分类器跑成了」**:绝大多数 ask 根本没咨询过分类器(部署没武装 auto、或这只 ask 走的
|
|
11
|
+
* 是别的门)。缺席只意味着「这只 ask 上没有这条事实」。
|
|
12
|
+
*
|
|
13
|
+
* -- 🔴 两条 cause 轴,不是一张表 --------------------------------------------------------------
|
|
14
|
+
* · {@link AUTO_MODE_UNAVAILABLE_CAUSES}(`error` / `timeout` / `breaker_open`)—— 「这一轮分类
|
|
15
|
+
* **为什么没跑成**」。这是 `classifierUnavailable.cause` 的值域。
|
|
16
|
+
* · {@link AUTO_MODE_BREAKER_CAUSES}(`error` / `timeout` / `parse_error`)—— 「**熔断闩为什么合上**」。
|
|
17
|
+
* 两集交于 `error` / `timeout`,各有一个独占成员。合成一张表就把两条轴的差别扔了。
|
|
18
|
+
* 🔴 **`parse_error` 只在熔断轴上**:core 顶注逐字 ——「`parse_error` stamps nothing」:分类器
|
|
19
|
+
* **跑了并且答了**,只是答在契约之外,那是**另一句话**。所以它**一个字节都不 stamp** 到
|
|
20
|
+
* `classifierUnavailable` 上;本读器按 unavailable 那一集收窄,读到 `parse_error` 判**缺席** ——
|
|
21
|
+
* 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
22
|
+
*
|
|
23
|
+
* -- 为什么是镜像而不是 import ---------------------------------------------------------------
|
|
24
|
+
* 这两张表与 `classifierUnavailable` 的型面在 sdk 8.8.0 与 agent-types 上**都还没有**(亲验:两棵树
|
|
25
|
+
* 全树零命中),唯一的出处是 `@sema-agent/core` 的 `dist/core/auto-mode.js` / `tool-policy.d.ts` /
|
|
26
|
+
* `checkpoint-store.d.ts`。而 core **不是本包消费者的依赖**(既非 peer 也非 runtime dep),本包的
|
|
27
|
+
* `.d.ts` 一旦引用它,装了本包却没装 core 的下游会当场编译不过。⇒ 与 `engineNoticeCodes.ts` /
|
|
28
|
+
* `toolResult.ts` / `retryStatus.ts` 同一条处置:**按真字节镜像,把代价交给门** ——
|
|
29
|
+
* `run-auto-mode-unavailable-test.mjs` 对**实装 devDep core** 的产物逐词双向对账,core 一动这里就先红。
|
|
30
|
+
* 🔴 **候上游导出即换**:sdk 哪天镜像了这两张表与那一格,本模块的表应当整只退役改成从 sdk 取。
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* 一轮分类**为什么没跑成**(core `AUTO_MODE_UNAVAILABLE_CAUSES`;逐词逐序镜像)。
|
|
34
|
+
* · `error` —— 模型那条腿抛了/被拒(分类时的路由失败也读在这里:派生路由的前置在任何 decide
|
|
35
|
+
* 之前就回落了,所以没有单独的词);
|
|
36
|
+
* · `timeout` —— 往返上限到了;
|
|
37
|
+
* · `breaker_open` —— 本会话的熔断闩**已经**合上,这一轮被短路,压根没发出去。
|
|
38
|
+
|
|
39
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
40
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
41
|
+
*/
|
|
42
|
+
export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* **熔断闩为什么合上**(core `AUTO_MODE_BREAKER_CAUSES`;逐词逐序镜像)——**另一条轴**,
|
|
45
|
+
* 与上面那张表刻意不合并(见模块顶注)。`parse_error` 是它的独占成员。
|
|
46
|
+
|
|
47
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
48
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
49
|
+
*/
|
|
50
|
+
export declare const AUTO_MODE_BREAKER_CAUSES: readonly string[];
|
|
51
|
+
/** 「分类器这次跑不了」的事实(只有成因一格 —— 它是显示元数据,不是裁决位)。 */
|
|
52
|
+
export interface ClassifierUnavailableView {
|
|
53
|
+
/** {@link AUTO_MODE_UNAVAILABLE_CAUSES} 之一。 */
|
|
54
|
+
cause: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 一只 ask(或一条 durable park 行的 `tool_approval` 载荷)→ 「分类器跑不了」的事实;
|
|
58
|
+
* 没有这条事实 / 形坏 ⇒ `undefined`,绝不抛出。
|
|
59
|
+
*
|
|
60
|
+
* 🔴 **两处同一只读器**:`AskRequest.classifierUnavailable` 与
|
|
61
|
+
* `PendingAction.tool_approval.classifierUnavailable`(durable park 行的孪生位)**键路同形**,
|
|
62
|
+
* 所以一只读器吃两处 —— 各写一份就是两份台账各漂各的,本包一贯要根治的形。
|
|
63
|
+
* 🔴 **`cause` 按闭集读**(unavailable 那一集):它是消费端分支的键。表外词(含
|
|
64
|
+
* **`parse_error`** —— 那是熔断轴的独占成员,core 明说它不 stamp 到这里)一律判**缺席**,
|
|
65
|
+
* 绝不当成一个能拿去渲「没跑成」的成因。
|
|
66
|
+
* 🔴 **只交 `cause` 一格**:顺手把整只 ask 的别的键带出来会长成第二份 ask 读面。
|
|
67
|
+
*/
|
|
68
|
+
export declare function classifierUnavailableOf(ask: unknown): ClassifierUnavailableView | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* 一个成因词 → 一句人话。**唯一措辞铸点**(三端共用;端零自拼)。
|
|
71
|
+
*
|
|
72
|
+
* 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
|
|
73
|
+
* 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
|
|
74
|
+
* 🔴 表外词 / 坏值 ⇒ 一句**兜底**:仍然告诉用户「这只 ask 是分类器那条腿引出来的」,但**不冒充**
|
|
75
|
+
* 四句里的任何一句(成因读不懂 ≠ 成因是别的什么);原样带上那个词供运维追问上游。
|
|
76
|
+
*/
|
|
77
|
+
export declare function classifierUnavailableDetail(cause: unknown): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/autoModeUnavailable.ts — 「这只 ask 是因为**分类器跑不了**才问人」的事实读器 + 唯一措辞铸点
|
|
3
|
+
* (0.63.0 件⑧;core 7.10.0 #616)。
|
|
4
|
+
*
|
|
5
|
+
* -- 它答的是哪一问 ---------------------------------------------------------------------------
|
|
6
|
+
* auto 模式下,门会就一只 ask 去咨询分类器。分类器**没跑成**时,引擎在这只 ask 上盖一格
|
|
7
|
+
* `classifierUnavailable: { cause }` —— 卡上那句「问你是因为分类器这次跑不了(超时)」的事实来源。
|
|
8
|
+
* 🔴 **它是显示元数据,不是裁决位**:没有任何东西读它来决定放行与否(引擎在门上已经判完了),
|
|
9
|
+
* 所以消费端只拿它渲一句话,**绝不**据它翻转任何裁决。
|
|
10
|
+
* 🔴 **缺席 ≠「分类器跑成了」**:绝大多数 ask 根本没咨询过分类器(部署没武装 auto、或这只 ask 走的
|
|
11
|
+
* 是别的门)。缺席只意味着「这只 ask 上没有这条事实」。
|
|
12
|
+
*
|
|
13
|
+
* -- 🔴 两条 cause 轴,不是一张表 --------------------------------------------------------------
|
|
14
|
+
* · {@link AUTO_MODE_UNAVAILABLE_CAUSES}(`error` / `timeout` / `breaker_open`)—— 「这一轮分类
|
|
15
|
+
* **为什么没跑成**」。这是 `classifierUnavailable.cause` 的值域。
|
|
16
|
+
* · {@link AUTO_MODE_BREAKER_CAUSES}(`error` / `timeout` / `parse_error`)—— 「**熔断闩为什么合上**」。
|
|
17
|
+
* 两集交于 `error` / `timeout`,各有一个独占成员。合成一张表就把两条轴的差别扔了。
|
|
18
|
+
* 🔴 **`parse_error` 只在熔断轴上**:core 顶注逐字 ——「`parse_error` stamps nothing」:分类器
|
|
19
|
+
* **跑了并且答了**,只是答在契约之外,那是**另一句话**。所以它**一个字节都不 stamp** 到
|
|
20
|
+
* `classifierUnavailable` 上;本读器按 unavailable 那一集收窄,读到 `parse_error` 判**缺席** ——
|
|
21
|
+
* 把它读成一个「没跑成」的成因就是替引擎编一件它明说没发生的事。
|
|
22
|
+
*
|
|
23
|
+
* -- 为什么是镜像而不是 import ---------------------------------------------------------------
|
|
24
|
+
* 这两张表与 `classifierUnavailable` 的型面在 sdk 8.8.0 与 agent-types 上**都还没有**(亲验:两棵树
|
|
25
|
+
* 全树零命中),唯一的出处是 `@sema-agent/core` 的 `dist/core/auto-mode.js` / `tool-policy.d.ts` /
|
|
26
|
+
* `checkpoint-store.d.ts`。而 core **不是本包消费者的依赖**(既非 peer 也非 runtime dep),本包的
|
|
27
|
+
* `.d.ts` 一旦引用它,装了本包却没装 core 的下游会当场编译不过。⇒ 与 `engineNoticeCodes.ts` /
|
|
28
|
+
* `toolResult.ts` / `retryStatus.ts` 同一条处置:**按真字节镜像,把代价交给门** ——
|
|
29
|
+
* `run-auto-mode-unavailable-test.mjs` 对**实装 devDep core** 的产物逐词双向对账,core 一动这里就先红。
|
|
30
|
+
* 🔴 **候上游导出即换**:sdk 哪天镜像了这两张表与那一格,本模块的表应当整只退役改成从 sdk 取。
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* 一轮分类**为什么没跑成**(core `AUTO_MODE_UNAVAILABLE_CAUSES`;逐词逐序镜像)。
|
|
34
|
+
* · `error` —— 模型那条腿抛了/被拒(分类时的路由失败也读在这里:派生路由的前置在任何 decide
|
|
35
|
+
* 之前就回落了,所以没有单独的词);
|
|
36
|
+
* · `timeout` —— 往返上限到了;
|
|
37
|
+
* · `breaker_open` —— 本会话的熔断闩**已经**合上,这一轮被短路,压根没发出去。
|
|
38
|
+
|
|
39
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
40
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
41
|
+
*/
|
|
42
|
+
export const AUTO_MODE_UNAVAILABLE_CAUSES = Object.freeze(['error', 'timeout', 'breaker_open']);
|
|
43
|
+
/**
|
|
44
|
+
* **熔断闩为什么合上**(core `AUTO_MODE_BREAKER_CAUSES`;逐词逐序镜像)——**另一条轴**,
|
|
45
|
+
* 与上面那张表刻意不合并(见模块顶注)。`parse_error` 是它的独占成员。
|
|
46
|
+
|
|
47
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
48
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
49
|
+
*/
|
|
50
|
+
export const AUTO_MODE_BREAKER_CAUSES = Object.freeze(['error', 'timeout', 'parse_error']);
|
|
51
|
+
const UNAVAILABLE = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
|
|
52
|
+
/**
|
|
53
|
+
* 一只 ask(或一条 durable park 行的 `tool_approval` 载荷)→ 「分类器跑不了」的事实;
|
|
54
|
+
* 没有这条事实 / 形坏 ⇒ `undefined`,绝不抛出。
|
|
55
|
+
*
|
|
56
|
+
* 🔴 **两处同一只读器**:`AskRequest.classifierUnavailable` 与
|
|
57
|
+
* `PendingAction.tool_approval.classifierUnavailable`(durable park 行的孪生位)**键路同形**,
|
|
58
|
+
* 所以一只读器吃两处 —— 各写一份就是两份台账各漂各的,本包一贯要根治的形。
|
|
59
|
+
* 🔴 **`cause` 按闭集读**(unavailable 那一集):它是消费端分支的键。表外词(含
|
|
60
|
+
* **`parse_error`** —— 那是熔断轴的独占成员,core 明说它不 stamp 到这里)一律判**缺席**,
|
|
61
|
+
* 绝不当成一个能拿去渲「没跑成」的成因。
|
|
62
|
+
* 🔴 **只交 `cause` 一格**:顺手把整只 ask 的别的键带出来会长成第二份 ask 读面。
|
|
63
|
+
*/
|
|
64
|
+
export function classifierUnavailableOf(ask) {
|
|
65
|
+
if (typeof ask !== 'object' || ask === null || Array.isArray(ask))
|
|
66
|
+
return undefined;
|
|
67
|
+
const raw = ask.classifierUnavailable;
|
|
68
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
69
|
+
return undefined;
|
|
70
|
+
const cause = raw.cause;
|
|
71
|
+
if (typeof cause !== 'string' || !UNAVAILABLE.has(cause))
|
|
72
|
+
return undefined;
|
|
73
|
+
return { cause };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 逐成因一句人话。**四句**(两轴的并集)刻意逐字互异 —— 对用户是四条不同的下一步。
|
|
77
|
+
* 🔴 `parse_error` 那一句说的是「分类器**跑了、答了**,只是答在契约之外」,与另外三句的
|
|
78
|
+
* 「没跑成」是两回事;它**永远不会**从 {@link classifierUnavailableOf} 出来(那一格按
|
|
79
|
+
* unavailable 轴收窄),放在这里是给**渲熔断面**的消费端用同一个铸点。
|
|
80
|
+
*/
|
|
81
|
+
const CAUSE_SENTENCES = Object.freeze({
|
|
82
|
+
error: 'asked because the auto-mode classifier could not run (it errored)',
|
|
83
|
+
timeout: 'asked because the auto-mode classifier could not run (it timed out)',
|
|
84
|
+
breaker_open: 'asked because the auto-mode classifier is latched off for this session (its breaker tripped), so this round was never sent',
|
|
85
|
+
parse_error: 'the auto-mode classifier answered outside its contract (its reply could not be parsed)',
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* 一个成因词 → 一句人话。**唯一措辞铸点**(三端共用;端零自拼)。
|
|
89
|
+
*
|
|
90
|
+
* 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
|
|
91
|
+
* 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
|
|
92
|
+
* 🔴 表外词 / 坏值 ⇒ 一句**兜底**:仍然告诉用户「这只 ask 是分类器那条腿引出来的」,但**不冒充**
|
|
93
|
+
* 四句里的任何一句(成因读不懂 ≠ 成因是别的什么);原样带上那个词供运维追问上游。
|
|
94
|
+
*/
|
|
95
|
+
export function classifierUnavailableDetail(cause) {
|
|
96
|
+
const known = typeof cause === 'string' && Object.hasOwn(CAUSE_SENTENCES, cause) ? CAUSE_SENTENCES[cause] : undefined;
|
|
97
|
+
if (known !== undefined)
|
|
98
|
+
return known;
|
|
99
|
+
const word = typeof cause === 'string' && cause.length > 0 ? cause : '(none)';
|
|
100
|
+
return `asked because of the auto-mode classifier; its reported cause ${word} is a word newer than this client`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/engineIdentity.ts — 引擎**代际锚**读器(`/health` 的 `pid` / `instanceId` / `startedAt`)
|
|
3
|
+
* 三端共用件(0.63.0;engine ≥7.67.0 / sdk 8.8.0 / S-179)。
|
|
4
|
+
*
|
|
5
|
+
* ── 为什么这一面值得一个模块 ─────────────────────────────────────────────────────────────────
|
|
6
|
+
* `/health` 是一台 worker **唯一免凭证**的门,心跳恒绿。于是「另一个宿主把这台共用引擎重启了」
|
|
7
|
+
* 此前只能靠某个**带凭证**的请求先撞上 401 才被发现 —— 而那条路会把一次重启误读成网络故障
|
|
8
|
+
* (处置完全相反:一个要重新握手,一个要重试)。engine 7.67.0 起 `/health` 无条件带上
|
|
9
|
+
* `startedAt`(这个**进程**自己的起点,epoch ms,模块加载时按 `process.uptime()` 铸一次),
|
|
10
|
+
* 换代因此可以被当成**一等事实**读出来,而不是等一次失败。
|
|
11
|
+
*
|
|
12
|
+
* ── 归层:本模块只读与比,不做状态机 ────────────────────────────────────────────────────────
|
|
13
|
+
* 「读出换代之后要做什么」(丢连接、重握手、提示用户)是**宿主的状态机**,壳有自己的那一只闸。
|
|
14
|
+
* 本包给的是两件纯物:一只窄读器 {@link engineIdentityOf} 与一族纯比较
|
|
15
|
+
* ({@link engineIdentityVerdict} / {@link engineIdentityChanged} / {@link engineIdentityChangedBy})。
|
|
16
|
+
* 三端共用同一套判据,才不会一端把「判不出」当「没换」、另一端把它当「换了」。
|
|
17
|
+
*
|
|
18
|
+
* ── 🔴 三态,不是布尔 ───────────────────────────────────────────────────────────────────────
|
|
19
|
+
* 一个布尔把「没换」与「判不出」压进同一个 `false`,而这两件事的下一步相反(前者继续用,后者
|
|
20
|
+
* 要么再探一次要么按最坏情况握手)。所以真源是三态判词;布尔口保留是因为大多数调用点只关心
|
|
21
|
+
* 「有没有**正面观察到**换代」,它的 `true` 是一句断言、`false` **不是**
|
|
22
|
+
* ([honest-absence-not-fabricated-zero])。
|
|
23
|
+
*
|
|
24
|
+
* ── 🔴 三只锚并列,不是「startedAt 说了算」───────────────────────────────────────────────────
|
|
25
|
+
* `instanceId` 答「是不是同一条命」,`startedAt` 还答「从什么时候起」——上游声明里逐字写着两者
|
|
26
|
+
* **刻意不合并**。所以判据是:**任一**在两侧都在场的锚不同 ⇒ 换代。只看 startedAt 会把
|
|
27
|
+
* 「startedAt 相同而 instanceId 不同」这类真实的坏读数判成「同一条命」。优先序
|
|
28
|
+
* ({@link ENGINE_IDENTITY_ANCHORS})只决定**判词报哪一只**,不决定看不看别的。
|
|
29
|
+
*/
|
|
30
|
+
/** 一台 worker 的代际锚读数。三格**并列**,各自独立在场/缺席。 */
|
|
31
|
+
export interface EngineIdentityView {
|
|
32
|
+
/** 这台 worker 的进程号(server 无条件发)。 */
|
|
33
|
+
pid?: number;
|
|
34
|
+
/** 这条命的实例 id(答「是不是同一条命」)。 */
|
|
35
|
+
instanceId?: string;
|
|
36
|
+
/** 这个进程的起点,epoch ms(server ≥7.67.0 无条件发;答「从什么时候起」)。
|
|
37
|
+
* 🔴 缺席 = **这台 worker 说不出来**(<7.67.0),**绝不是**「它没重启过」。 */
|
|
38
|
+
startedAt?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 代际锚的**优先序**。序:`startedAt`(最具体:同一条命还答得出从何时起)→ `instanceId`
|
|
42
|
+
* (同一条命吗)→ `pid`(端口上还是那个进程吗)。
|
|
43
|
+
*
|
|
44
|
+
* 🔴 **顺序**只决定 {@link engineIdentityChangedBy} 报哪一只;但**成员集合是承重的** ——
|
|
45
|
+
* 比较器**遍历的正是这张表**,少一只锚就等于那只锚不再参与判定(异源对抗复审 R2 [medium] 实撞:
|
|
46
|
+
* 移掉 `instanceId` 之后,两份 `instanceId` 不同、别的锚相同的合法读数从 `changed` 翻成 `same`,
|
|
47
|
+
* 宿主据此漏掉一次重握手)。
|
|
48
|
+
* 🔴 因此它是 `Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]`:后者一行
|
|
49
|
+
* `.splice()` 就能改,而公面消费者拿到的正是这个实例。这是本仓**已定谳的病形**
|
|
50
|
+
* (`RESUME_RETRY_LATER_CODES` / `RUN_LEVEL_STOP_ERROR_CODES` 两次同款处置)。
|
|
51
|
+
* 运行期反钉在 `run-engine-identity-test.mjs` 的 Bz 段(试改 + 试改后判词没漂,两半都断言)。
|
|
52
|
+
*/
|
|
53
|
+
export declare const ENGINE_IDENTITY_ANCHORS: readonly (keyof EngineIdentityView)[];
|
|
54
|
+
/**
|
|
55
|
+
* `/health` 200 体 → 代际锚读数;**畸形一律缺席**,绝不抛出。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 **逐格独立**:一只锚形坏只丢那一格(三只锚是三条独立的证据,一只坏不该把另两只藏起来)。
|
|
58
|
+
* 🔴 **恒返回一只读数对象**(读不出任何一格时是 `{}`),不返回 `undefined` —— 调用方读的是
|
|
59
|
+
* 「哪几只锚这次答得出来」,而不是「这次有没有响应」(后者是它自己的探测腿知道的事)。
|
|
60
|
+
* ⚠️ 只取三只锚;`/health` 上其余的键(`version` / `configHash` / `dataRoot` / 降级位…)各有
|
|
61
|
+
* 自己的读面,不在这里搭便车 —— 一个「顺手多带两个键」的读数会变成第二份 health 台账。
|
|
62
|
+
*/
|
|
63
|
+
export declare function engineIdentityOf(health: unknown): EngineIdentityView;
|
|
64
|
+
/** 换代判词。`unknown` = 两侧**没有一只锚可比**(诚实缺席,绝不折成 `same`)。 */
|
|
65
|
+
export type EngineIdentityVerdict = 'changed' | 'same' | 'unknown';
|
|
66
|
+
/**
|
|
67
|
+
* 两份读数说的是不是**同一条命**。
|
|
68
|
+
*
|
|
69
|
+
* 判据(与 {@link engineIdentityChangedBy} 同一套,只是那一只多报一个名字):
|
|
70
|
+
* · **任一**在两侧都在场的锚**不同** ⇒ `changed`;
|
|
71
|
+
* · 至少一只锚可比、且没有一只不同 ⇒ `same`;
|
|
72
|
+
* · **一只可比的锚都没有** ⇒ `unknown`。
|
|
73
|
+
*
|
|
74
|
+
* 🔴 **锚交集为空 ⇒ `unknown` 而不是 `same`**:老 worker 只报 `pid`、新 worker 只报 `startedAt`
|
|
75
|
+
* 时,「没发现不同」是因为**没得比**,不是因为它没换。把这一格读成 `same` 正是本模块要根治的
|
|
76
|
+
* 那类静默病。
|
|
77
|
+
* 🔴 单侧在场的锚**不是反证**:一侧有 `startedAt` 另一侧没有,只说明其中一台答不出这只锚;
|
|
78
|
+
* 只要另有一只锚可比且相同,判词照给 `same`。
|
|
79
|
+
*/
|
|
80
|
+
export declare function engineIdentityVerdict(a: unknown, b: unknown): EngineIdentityVerdict;
|
|
81
|
+
/**
|
|
82
|
+
* 换代是**哪一只锚**看出来的(优先序最高的那一只不同的锚)。没换 / 判不出 ⇒ `undefined`。
|
|
83
|
+
* 给的是一句人可读的诊断线索(「端口上换了个进程」vs「同一个进程报了另一条命」),
|
|
84
|
+
* **不是**裁决位 —— 裁决位是 {@link engineIdentityVerdict}。
|
|
85
|
+
*/
|
|
86
|
+
export declare function engineIdentityChangedBy(a: unknown, b: unknown): keyof EngineIdentityView | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* **正面观察到换代了吗**。
|
|
89
|
+
* 🔴 `true` 是一句断言(某只可比的锚确实变了);`false` **不是** —— 它同时覆盖「没换」与
|
|
90
|
+
* 「判不出」。要分清这两件事,读 {@link engineIdentityVerdict}(那是本族的真源)。
|
|
91
|
+
* 这个方向是刻意的:据 `true` 做的事(重新握手)在误判时代价可控,据 `false` 做的事
|
|
92
|
+
* (继续用这条连接)在误判时会一直对着一台已经换掉的引擎说话。
|
|
93
|
+
*/
|
|
94
|
+
export declare function engineIdentityChanged(a: unknown, b: unknown): boolean;
|