@sema-agent/settings-schema 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,127 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 8.2.0 — 2026-10-08 — 迁移器逐行终判(行为改真)· fleet 端点常量与活性路由组 · 心跳三键收型 · 中心能力位的承载键 · 两处注释改真
10
+
11
+ > **判档 = minor**(README「发版纪律」:新键 / 新导出 = minor)。新导出 6 名(`LEASE_STATES` / `LeaseState` / `FLEET_PATHS` / `FLEET_LIVE_PATHS` /
12
+ > `HEARTBEAT_REPLY_CAPABILITIES_KEY`,`.` 与 `./fleet` 各 +5;`LeaseState` 是值 + 类型)+ 一个资产出口 `./fleet-paths.json`;`EngineSignal` 与 `InstanceAnnouncement`
13
+ > 各多可选键(无默认)。**唯一一处行为改真**是第一节的迁移器:它改变的是**读侧本来就拒**的文档的迁移结局(多一类 `refused` 行),合法文档的迁移结局与
14
+ > 读数一字不变(金样 m01–m04 与全部语料零 diff)。导出面登记表只多名字行,无删、无改形。
15
+
16
+ ### 一、`migrateWorkersDomain` 逐行终判(行为改真:多一类 refused 行)
17
+
18
+ **病**(管理面实测):一份已是 7.x 形、三行都不需要迁的 workers 文档里,一台在 `env` 里设了 `FLEET_GENERATION`(8.1.0 起的保留名)——
19
+ 迁移回 `changed: []` / `refused: []`,随后 `parseDomain("workers")` 整域抛:管理面 workers 唯一读口整域读不出,一行坏把整份舰队清单拖黑;
20
+ CLI 对同一份文件报 `outcome=noop` / 退出 0。根因:迁移的终判(`WorkerSpec.safeParse`)只对**它改写过的行**跑,没动的行直接放行 ——
21
+ 于是迁移与读侧对同一行各说各话,任何「存下来时合法、后来被收紧的判据拒掉」的行(保留名表变长只是其中一例)都走这条缝。
22
+
23
+ **修**(规则替换,不是加一条):终判**逐行**,迁过的行与没动的行同一只 —— 每一只认得出名字(字符串 `name`)的 worker 都过读侧自己的判据:
24
+ **只含这一行的 workers 域**读不读得进(`WorkersConfig`,就是 `parseDomain("workers")` 用的那一只 schema;逐行的规则写在 `WorkerSpec` 里的、
25
+ 写在域层的(`replicas` 与 `autoscaling` 互斥)都在,跨行规则一行时不触发)。读不进 ⇒ `refused`(`name` + `field` = schema issue 相对该行的路径 +
26
+ `reason` = schema 自己的原句,前缀说明是迁出形 `after migration the worker …` 还是存储形 `the stored worker …`;`original` 照旧逐字带回)。
27
+ 迁移不复述任何一条写闸规则、不另抄保留名表。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(迁移不判形,原样留在 `doc`)与跨行规则
28
+ (名字 / 路由前缀唯一)。迁过的行的终判拒因原句从 `after migration the worker is not a valid 7.0.0 WorkerSpec — …` 改成 `… is not a valid WorkerSpec — …`
29
+ (判据是当前版本的 schema,8.1.0 的保留名也在里面,「7.0.0」已不准)。
30
+
31
+ **拒因不回显输入值**(车内对抗复审的发现,修在同一版):zod 自带的枚举拒句以 `, received '<填的值>'` 收尾 —— 迁过的行的终判拒因与 CLI 整域终判的报告
32
+ 早就会带出它(8.1.0 已有),逐行终判又把没动的行也接到这条路上。现在 issue 一律经同一只渲染(内部 `issueText`,不在任何子路径上):
33
+ 枚举拒句只按它的闭集重写成 `Invalid enum value. Expected 'a' | 'b'`,其余 issue 的句子照旧(zod 的其余默认句只说期望的形,不带输入)。
34
+ 迁移的 `refused[].reason`、CLI 报告的 `refused[].reason` 与 stderr 结论句三处同用。
35
+
36
+ ### 二、心跳三键收型:`LEASE_STATES` / `LeaseState` · `EngineSignal.leaseState?` · `InstanceAnnouncement` 不再剥三键(additive)
37
+
38
+ - **`LEASE_STATES = ["none", "granted", "expired", "denied"]`**(+ zod `LeaseState` 与同名类型)—— 引擎持有的服务租约四态,闭集在这里定一次(引擎的词表逐字同序):
39
+ `none` = 租约不生效(中心没宣告 `servingLease` 位;行为与没有租约的引擎逐字同)· `granted` = 持有租约(`leaseUntil` 说到何时)·
40
+ `expired` = 租约生效但此刻没有有效授予(中心不可达 / 答 `unknown` / 尚未首授;照常服务直到引擎自己的不可达上限,不是被拒)·
41
+ `denied` = 中心凭正面证据拒了本实例(黏性,已排空、到期自停)。只有 fleet 成员说它;非成员没有这个键(没有第五个词表示「非成员」,缺席就是)。
42
+ - **`EngineSignal.leaseState?`**:闭形多一键(引的就是 `LeaseState` 那一只 schema);`leaseUntil` 的注改准为「在场 ⇔ `leaseState` 是 `granted`」。
43
+ `readEngineSignal` 原样读出;集外词按 `EngineSignal` 既有的律整只 `undefined`。
44
+ - **`InstanceAnnouncement` 加 `generation?` / `leaseState?` / `leaseUntil?`**(心跳此前把这三键 strip 掉 —— 引擎早已在每拍心跳上平铺它们,中心读不到):
45
+ 三键各自**引用** `EngineSignal` 上同名键的那一只 schema(规范代际串 / `LeaseState` / 非负整数 ms),不抄第二份;存下来的行(`InstanceRegistration`)随之带着。
46
+ **失败方向随含义**:三键是中心留存的证据、不是路由输入 —— 形坏的那一键落成缺席(`.catch`,与 `modelRefusals` 同姿势),心跳照收、可路由性不变;
47
+ 所以引擎以后多一个租约词,老中心不会因此拒掉它的每一拍心跳。三键之间的在场律(成员 ⇒ `generation` + `leaseState`;`leaseUntil` ⇔ `granted`)是引擎的律,本形不跨键判。
48
+ - 金样:引擎今天真发的心跳体(成员 · granted / expired 两拍、单机形)入 `test/fleet-8.2.0.test.ts`。
49
+
50
+ ### 三、两处注释改真(零行为)
51
+
52
+ - **`EngineSignal` 头注**:8.1.0 写的「心跳与 `/health` 携带同名键、同值,平铺在体上,`readEngineSignal` 读哪一个都行」(本档 8.1.0 第二节同句)**不成立**,
53
+ 且与同一份注里「心跳 / `InstanceAnnouncement` 的 `draining` 是布尔」自相矛盾。照实改为:两出口今天都不整只承载这只信号 ——
54
+ ① 心跳恒带布尔 `draining`(含 `false`),`/health` 只在排空时带 `draining: true`;② `/health` 只在**未**就绪时带 `ready`;③ 引擎把 `configTargetVersion` /
55
+ `configAppliedVersion` 写成数,本形是串。所以 `readEngineSignal` 对今天任何心跳体恒 `undefined`,对 `/health` 体在三条任一成立时 `undefined`;两出口同名同值平铺的
56
+ 只有 `restart` / `instanceId` / `generation` / `leaseState` / `leaseUntil`(心跳上读解析后的 `InstanceAnnouncement`)。下一个 major 退布尔对、两出口整只承载、两只路由谓词改吃它。
57
+ `readEngineSignal` 的注同步(不再说「两出口键都平铺、哪个都能读」)。README / llms-full 的同句一并改。
58
+ - **`ServingLeaseRequest.scope` 注**:8.1.0 写「缺席 = 单 scope 部署」。改成续租的令牌规则一句话:续租**只收 per-worker 令牌**(凭证绑定 (scope, worker)),
59
+ 所以 scope 由凭证定、本字段只能附和 —— 缺席 = 令牌绑定的 scope;在场必须与之相等(不符即拒,两头都不改写);**共享令牌不得续租**(中心答 403)。
60
+ - **心跳一侧的 scope 规则写在 `InstanceAnnouncement` 注上**:心跳体**没有** `scope` 键(本形没有这个键,发了也被 strip);行落在凭证点名的 scope ——
61
+ per-worker 令牌 = 令牌绑定的 scope(给了 `?scope=` 就必须相等);共享令牌 = `?scope=`,缺席 ⇒ `"global"`。这条是心跳的,不是续租的。
62
+ - 钉:`test/fleet-8.2.0.test.ts` ③ —— 四格注释文本钉(续租注不许出现 `?scope=` / `"global"`;在改前的注上红)+ 两格行为钉(引擎今天真发的心跳 / `/health` 金样上
63
+ `readEngineSignal` 的读数,本来就绿:注改的是话,不是行为);心跳注那一格顺带钉「心跳体里的 `scope` 被 strip」。
64
+
65
+ ### 四、fleet 端点常量:`FLEET_PATHS` · `FLEET_LIVE_PATHS` · 随包的 `fleet-paths.json`(additive)
66
+
67
+ **病**:fleet 端点路径两仓各拼一份,已经对不上 —— 引擎续租打 `/api/fleet/serving-lease`,管理面活性组预留的是 `/api/fleet/live/serving-lease`
68
+ (中心一宣告 `servingLease` 位,全舰队续租就 404);心跳 / 用量 / 配额租约三条也是两侧手写字面。
69
+
70
+ - **`FLEET_PATHS`**(`as const`,形照 `AUTH_V1_PATHS` 先例)—— 中心外部按路径打到的 `/api/fleet/*` 端点,一种拼法:
71
+ `instances: "/api/fleet/instances"`(心跳 `POST` / 告别 `DELETE`)· `usage: "/api/fleet/usage"` · `quotaLeases: "/api/fleet/leases"` ·
72
+ `servingLease: "/api/fleet/live/serving-lease"` · `hostPlanner: "/api/fleet/live/host-planner/{host}"`(执行器读一台 host 的规划记录;体是中心与执行器的契约,
73
+ 不在本包 —— 它在这里是因为部署要把它当活性组成员路由)。成员律:本包定形其体的端点 ∪ 活性组成员;中心内部路由不在。
74
+ 记法:值是路径**模板**,参数只以整段 `{name}` 出现(只有 `hostPlanner` 有);前缀路由(Ingress)取第一个 `{` 之前的部分。
75
+ **没有能力读口端点**:中心能力位的唯一载体是心跳 2xx 答复体(见第五节)。
76
+ - **`FLEET_LIVE_PATHS`** = `[instances, servingLease, hostPlanner]` —— 活性路由组:中心不经就绪门、不经配置治理门作答的那组(不读配置域、只收服务凭证),
77
+ 部署把它们路由过就绪门(Kubernetes:第二个 `publishNotReadyAddresses` 的 Service,Ingress 规则指向它)。**组是这张表,不是路径前缀**(心跳早于 `/api/fleet/live/`
78
+ 前缀);`FLEET_PATHS` 里 `/api/fleet/live/` 下的每一条都在组里;配额租约与用量不在。值就是 `FLEET_PATHS` 里那几只串,不是第二份拼写。
79
+ - **`@sema-agent/settings-schema/fleet-paths.json`**(新资产出口 → `dist/fleet-paths.json`)= `{ "FLEET_PATHS": …, "FLEET_LIVE_PATHS": … }`:给 import 不了 TS 的工具
80
+ (Helm 模板、冒烟脚本)对表。`npm run build` 在 tsc 之后由 `scripts/gen-fleet-paths.mjs` 从**构建出的** `dist/fleet.js` 写出(脚本里没有路径字面),不手改;
81
+ 钉它与常量逐字同(常量改了没重建 ⇒ 红)。导出面登记表表头的资产出口清单改成从 exports 表派生(原先手写只有 `./package.json`)。
82
+ - 钉:`src/` 里 `"/api/fleet/` 起头的字面量只出现在 `FLEET_PATHS` 那几行(第二处拼写 ⇒ 红)。
83
+
84
+ ### 五、中心能力位的承载键:`HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities"`(additive)+ `CenterCapabilities` 注改真
85
+
86
+ - 中心把 `CenterCapabilities` 放在**心跳 2xx 答复体**的哪个键下 —— 这是能力对象的**唯一载体**(没有独立的能力读口;一事实一载体)。中心写
87
+ `{ …, [HEARTBEAT_REPLY_CAPABILITIES_KEY]: caps }`,引擎读 `readCenterCapabilities(reply[HEARTBEAT_REPLY_CAPABILITIES_KEY])`,且只在 2xx 时读;嵌套不平铺
88
+ (答复体自己的键与能力位不共享命名空间);答复体没有这个键 = 老中心,什么位都没说。两仓不再各写字面(`src/` 里这个键名字面量只出现在常量那一行,有钉)。
89
+ - `CenterCapabilities` 注:撤 8.1.0 的「本包只定形与读器、不定哪个中心端点承载」;改为「唯一载体 = 心跳 2xx 答复体的这个键」,并加一句
90
+ **位按调用方凭证答**:per-worker 令牌按中心实际支持的答;共享令牌下 `servingLease` 恒缺席(续租端点只收 per-worker 令牌,对共享令牌宣告只会把它推进不可达臂)。
91
+ - 金样:老中心 `{ ok: true }` ⇒ `undefined`;`{ ok: true, capabilities: {} }` ⇒ `{}`;`{ ok: true, capabilities: { servingLease: true } }` ⇒ 启用;位平铺在答复体上不算。
92
+
93
+ ### 用户可见变化(谁受影响 / 怎么改 / 不改会怎样)
94
+
95
+ 1. **管理面的 workers 读口(调 `migrateWorkersDomain` 的)**:一行设了保留名(或别的逐行读不进的形)的文档,以前整域读失败,现在那一行进 `refused`、其余照常加载。
96
+ 按契约「`refused` 行保住它正在跑的负载、不当删除」处理的,零动作;不改会怎样:没这样处理的编排器会把那一行当成缺席。
97
+ 2. **`migrate-workers` CLI 的脚本**:上面那类文件以前 `outcome=noop` / 退出 0,现在 `outcome=refused` / 退出 1、一字不写;读 `refused[].reason` 原句的,
98
+ 迁过的行的终判拒因少了「7.0.0」,没动的行的拒因以 `the stored worker` 起头;枚举拒句不再带 `received '<值>'`。
99
+ 3. **引擎(续租客户端)**:续租路径改 import `FLEET_PATHS.servingLease`(`/api/fleet/live/serving-lease`);不改会怎样:中心按包常量注册续租路由后,手写
100
+ `/api/fleet/serving-lease` 的引擎续租得 404(走不可达臂)。心跳答复体的能力对象改读 `reply[HEARTBEAT_REPLY_CAPABILITIES_KEY]`。
101
+ 4. **中心**:心跳 / 用量 / 配额租约 / 续租 / HostPlanner 的路由路径与活性组表从 `FLEET_PATHS` / `FLEET_LIVE_PATHS` 取;能力对象写在
102
+ `HEARTBEAT_REPLY_CAPABILITIES_KEY` 下;心跳解析结果里现在有 `generation` / `leaseState` / `leaseUntil`(存行 `InstanceRegistration` 随之)。
103
+ 5. **部署(Helm / 冒烟)**:活性组路由与对表读 `@sema-agent/settings-schema/fleet-paths.json`(`FLEET_LIVE_PATHS`;`{host}` 是路径参数,前缀路由取 `{` 之前)。
104
+ 6. **对 `EngineSignal` / `InstanceAnnouncement` 的键做穷举的**:多出的键编译红(提货成本,补一行)。**持 `LEASE_STATES` 穷举的**:以后加词同理。
105
+
106
+ ### 红先 / 门
107
+
108
+ - 每节先写钉、对着改前的树跑红,再实现:第一节 `test/migrate-workers-row-verdict-8.2.0.test.ts` 20 格(基线 14 红;6 绿是干净文档幂等、两格最后围栏、
109
+ 整域读侧对照等反向格;其中 3 格「拒因不回显值」是复审后补的,在复审前的树上红)+ 金样 m05;第二节 `test/fleet-8.2.0.test.ts` ④ 12 格(8 红;4 绿 = 旧形不铸键、失败方向、类型面等反向格);第三节 ③ 4 格注释文本钉
110
+ (改前的注上红;其中心跳注那一格顺带钉「心跳体里的 `scope` 被 strip」)+ 2 格行为钉(本来就绿:注改的是话);第四节 ② 11 格(常量格在缺常量的树上红,
111
+ JSON 三格随生成器同提交、以变异自证咬合);第五节 ⑤ 4 格(基线全红)。
112
+ `test/engine-signal-8.1.0.test.ts` 两格键集钉随 additive 加键改;`test/conformance-workers-7.0.0.test.ts` 金样清单四份 → 五份。
113
+ - 变异自证(26 个变异,每个都至少使预期那一格红):没动的行跳过终判 / 判据退回 `WorkerSpec` / 手抄保留名表 / `field` 不去前缀 / 拒因措辞对调 / `original` 被拷贝;
114
+ 心跳 `leaseState` 严判 / 抄第二份 / 改词 / `EngineSignal.leaseState` 收任意串 / 心跳 `generation` 不判规范 / 丢 `leaseUntil` / `EngineSignal` 无 `leaseState`;
115
+ 能力端点回来 / 活性组二次拼写 / 少 HostPlanner / 多配额租约 / 引擎侧旧续租拼法 / 冒号参数记法 / JSON 过期;键名改字 / 第二处字面 / 能力位注回旧句。
116
+ - 全量:build rc 0 · typecheck rc 0 · vitest 84 files / 1501 tests rc 0 · e2e rc 0 · `npm pack --dry-run` rc 0(360 文件,含 `dist/fleet-paths.json`,无 docs / scripts / test / src)。
117
+ - 导出面登记表(`docs/API-SURFACE.md`)重生成:`.` 与 `./fleet` 相对 8.1.0 各 +5 名,无删无改形;表头的资产出口清单改为从 exports 表派生。
118
+
119
+ ### 未闭环(如实)
120
+
121
+ - **`EngineSignal` 今天在两个出口上都读不出**(第三节):心跳的布尔 `draining`、`/health` 就绪时缺 `ready`、引擎把世代账两键写成数而本形是串 ——
122
+ 第三条是契约与引擎的形不一致,本版只照实写进注,不改形(改 `configTargetVersion` / `configAppliedVersion` 的型是收紧 = major);收口在下一个 major
123
+ 与引擎同版翻转。
124
+ - **心跳三键之间的在场律不判**(成员 ⇒ `generation` + `leaseState`;`leaseUntil` ⇔ `granted`):是引擎的律,本形逐键判、不跨键判。
125
+ - **`LEASE_STATES` 与引擎词表的对齐是人读的**:本仓没有取引擎源码的通道,词表逐字照引擎今天的四词定,没有机器门看「引擎加词而本包没跟」。
126
+ - **迁移器的 `field` 粒度随读侧 issue 路径**:保留名拒的 `field` 是 `["env"]` / `["secretEnv"]`(读侧的保留名判据挂在整张表上),键名在 `reason` 里;
127
+ 要指到 `["env", <键>]` 得改读侧的 issue 路径(诊断面,另一件)。
128
+ - **CLI 的 `noop` 不跑整域终判**:一份不需要迁、逐行都读得进、但跨行冲突(名字重复)的文件仍报 `noop` / 退出 0(既有行为,本版未动)。
129
+
9
130
  ## 8.1.0 — 2026-10-08 — 引擎自铸信号的共享型:`RestartSignal.withheld` · `EngineSignal` · 服务租约 API 型与中心能力位 · 排空请求 / 回执型 · fleet env 名常量(全部 additive)
10
131
 
11
132
  > **判档 = minor**(README「发版纪律」:新键 / 新导出 = minor)。本版只加**型与契约**,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement`
package/README.md CHANGED
@@ -48,13 +48,42 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
48
48
  > a zod-3 host (today the config-center product) still dedups to one instance. Moving this package to zod 4 is a major
49
49
  > of its own (S-777); 6.2.0 does not touch the dependency or peer range.
50
50
 
51
+ ## 升级指引:8.1.0 → 8.2.0(additive + 迁移器一处行为改真)
52
+
53
+ **行为改真(一处)—— `migrateWorkersDomain` 终判逐行**:每一只有字符串名字的 worker(迁过的、没动的一样)都要读得进「只含它一行的 workers 域」
54
+ (读侧的 `WorkersConfig`,不另抄保留名表),读不进就进 `refused`;以前只判迁过的行,没动的行直接放行。
55
+
56
+ - **谁需要**:管理面的 workers 唯一读口 —— 以前一份已是 7.x 形的文档里只要一行在 `env` 里设了保留名(例:8.1.0 起的 `FLEET_GENERATION`),迁移报零变化,
57
+ 随后 `parseDomain("workers")` 整域抛,整份舰队清单读不出;现在那一行进 `refused`(名字 + `field` + 读侧原句),其余行照常加载。文件后端 / 离线的
58
+ `migrate-workers` CLI 同理:那份文件以前报 `outcome=noop` / 退出 0(说「没事」),现在报 `outcome=refused` / 退出 1、一字不写、`refused[]` 点名那一行。
59
+ - **谁受伤**:① 读口把 `refused` 当「不加载」的消费方 —— 一行以前让整域读失败(什么都不加载),现在只有它不加载;按「`refused` 行保住它正在跑的负载、不当删除」
60
+ 处理的编排器(契约要求,见 `WorkersMigrationRefusal.original`)不受影响,没这样处理的会把这一行当成缺席。② 把 `migrate-workers` 退出 0 当「文件可用」的脚本:
61
+ 这类文件现在退出 1。③ 读 `refused[].reason` 原句的:迁过的行的终判拒因从 `… not a valid 7.0.0 WorkerSpec — …` 改成 `… not a valid WorkerSpec — …`,
62
+ 没动的行的拒因前缀是 `the stored worker …`。
63
+ - **补偿**:`refused[]` 逐条带 `name` / `index` / `field` / `reason` 与原行逐字(`original`),写回前按 `index` 放回 —— 运维的 spec 不丢;修法 = 改掉 `field` 指的那一格
64
+ (保留名:删掉 `env` / `secretEnv` 里那一键,由它的唯一写位产出)。整域读侧仍是最后围栏,只剩两类归它:没有字符串名字的条目与跨行唯一性(名字 / 路由前缀)。
65
+
66
+ **additive(零动作可升)**,全在 `./fleet`(与主入口)上:
67
+
68
+ - `LEASE_STATES` / `LeaseState` —— 引擎持有的服务租约四态闭集 `none | granted | expired | denied`。
69
+ - `EngineSignal.leaseState?`;`InstanceAnnouncement` 不再剥 `generation?` / `leaseState?` / `leaseUntil?`(引 `EngineSignal` 同名键那一只 schema;形坏的那一键落成缺席,心跳照收)。
70
+ - `FLEET_PATHS`(`instances` / `usage` / `quotaLeases` / `servingLease` = `/api/fleet/live/serving-lease` / `hostPlanner` = `/api/fleet/live/host-planner/{host}`)、
71
+ `FLEET_LIVE_PATHS`(活性路由组:心跳 / 续租 / HostPlanner 读口)与新资产出口 `@sema-agent/settings-schema/fleet-paths.json`(同两只常量的 JSON,构建时写)。
72
+ **引擎侧注意**:续租路径以 `FLEET_PATHS.servingLease` 为准(`/api/fleet/live/serving-lease`),不是 `/api/fleet/serving-lease`。
73
+ - `HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities"` —— `CenterCapabilities` 的唯一载体是心跳 2xx 答复体的这个键;位按调用方凭证答(共享令牌看不到 `servingLease`)。
74
+ - 注释改真:`EngineSignal`(今天的心跳与 `/health` 都不整只承载它 —— 心跳恒带布尔 `draining`,`readEngineSignal` 读今天的心跳体恒 `undefined`)、
75
+ `ServingLeaseRequest.scope`(续租只收 per-worker 令牌;缺席 = 令牌的 scope,在场必须相等;共享令牌不得续租)。
76
+
77
+ 提货成本:对 `EngineSignal` / `InstanceAnnouncement` 的键做穷举的消费方会因多出的键编译红;对 `LEASE_STATES` 穷举的,以后加词同理。逐条见 CHANGELOG 8.2.0。
78
+
51
79
  ## 升级指引:8.0.0 → 8.1.0(全部 additive,零动作可升)
52
80
 
53
81
  8.1.0 只在 `./fleet`(与主入口)上**加**型与契约,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement` 的形、两个就绪谓词一字未动。新增清单:
54
82
 
55
83
  - `RestartSignal.withheld?` —— 可选布尔,引擎自判「这一拍不该被换代」;缺席 = `false`,读出形不铸键。
56
84
  - `EngineSignal`(+ `EngineDraining`)与 `readEngineSignal(health)` —— 引擎自铸信号的**闭形**(`ready` 必填,`draining?` / `restart?` / `generation?` / `instanceId?` /
57
- `leaseUntil?` / `configTargetVersion?` / `configAppliedVersion?` 可选),键平铺在 `/health` 与心跳体上;与既有读器走同一只受保护读口。
85
+ `leaseUntil?` / `configTargetVersion?` / `configAppliedVersion?` 可选);与既有读器走同一只受保护读口。⚠️ 8.2.0 订正:今天的心跳与 `/health` 都不整只
86
+ 承载它(心跳恒带布尔 `draining`、`/health` 就绪时不带 `ready`、世代账两键是数),`readEngineSignal` 对今天的心跳体恒 `undefined` —— 见 8.2.0 升级指引。
58
87
  `undefined` = 没有这只信号,**不是就绪**;布尔 `draining` 的旧方言体读不出它(两个就绪谓词的输入仍是布尔方言,下一个 major 收)。
59
88
  - 代际 —— `generation` 是非负整数的**规范十进制串**(`"7"`,不是 `"07"`、不是数字型),判别器 `isCanonicalGeneration`、数值比较 `compareGeneration`;三处 wire 形共用这一条判据。
60
89
  - 服务租约 API 型 —— `ServingLeaseRequest`、`ServingLeaseResponse`(`granted { ttlMs }` / `denied { reason: superseded | retired }` / `unknown` 三词闭集,
@@ -128,7 +157,11 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
128
157
  `bin`; writes only when every worker migrates). Workers it cannot lift (an enabled worker with no
129
158
  `env.REMOTE_EXEC` — the migration never guesses a lane) come back in `refused` with the field to fix and the
130
159
  `original` row verbatim — they are NOT in the migrated `doc`, so a caller that writes `doc` back must put each
131
- `refused[].original` back at its `index` (or store it aside), or that worker's spec disappears.
160
+ `refused[].original` back at its `index` (or store it aside), or that worker's spec disappears. The final judgement
161
+ is per row (since 8.2.0 the same one for rows it rewrote and rows it left alone): every worker with a string name must read
162
+ as a one-row workers domain under the read side's own `WorkersConfig`, or it goes to `refused` — so each row of `doc`
163
+ reads on its own; what the whole-domain read can still refuse is a nameless entry (left as is) and the cross-row rules
164
+ (unique names / route prefixes).
132
165
  - **`migrate-workers` CLI: report shape + rejection signposts (7.0.1)** (`bin/settings-schema.mjs`, core in `cli.ts`):
133
166
  `npx @sema-agent/settings-schema migrate-workers <workers.json> [--check]` prints exactly two lines on stdout (exit 0 / 1 / 3): line 1 is the
134
167
  ONE conclusion line `outcome=<refused|applied|noop|pending> written=<true|false>`, line 2 is the JSON report (`event: "workers_domain_migrated"`,
@@ -294,7 +327,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
294
327
  optional `withheld` ("the engine says this beat must not be handed to a new generation"; absent ≡ false);
295
328
  `EngineSignal` (`{ ready, draining?: EngineDraining, restart?, generation?, instanceId?, leaseUntil?,
296
329
  configTargetVersion?, configAppliedVersion? }`) is the closed form of the engine-minted subset of the /health
297
- self-description (keys flat on the /health and heartbeat bodies; `InstanceHealth` stays the open record) and
330
+ self-description (`InstanceHealth` stays the open record; neither exit carries it whole yet — see its doc comment) and
298
331
  `readEngineSignal(health)` reads it through the same guarded reader as `readRestartSignal` / `readModelRefusals` —
299
332
  `undefined` means "no such signal", never "ready", and a body carrying the boolean `draining` of the 8.0 dialect is not
300
333
  one. The serving-lease wire (`ServingLeaseRequest` / `ServingLeaseResponse`: `granted { ttlMs }` | `denied { reason:
@@ -304,6 +337,13 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
304
337
  (`servingLease?: true` — absent means the center has no lease, so the engine neither renews nor is fenced), the drain
305
338
  endpoint's `DrainRequest` / `DrainReceipt`, one reader per wire shape, and the env name `FLEET_GENERATION_ENV` are defined
306
339
  here once for the renderer and the engine (an instance's id is minted by the engine per process — it has no env name).
340
+ **8.2.0** (additive) adds the engine's lease vocabulary `LEASE_STATES` (`none | granted | expired | denied`) as
341
+ `EngineSignal.leaseState?`, keeps `generation` / `leaseState` / `leaseUntil` on the heartbeat (`InstanceAnnouncement` no longer
342
+ strips them; a malformed one drops to absent, the heartbeat still lands), spells the center's fleet endpoints once —
343
+ `FLEET_PATHS` (heartbeat, usage, quota leases, serving lease `/api/fleet/live/serving-lease`, host planner
344
+ `/api/fleet/live/host-planner/{host}`) and the liveness group `FLEET_LIVE_PATHS` (heartbeat, serving lease, host planner), also
345
+ shipped as `@sema-agent/settings-schema/fleet-paths.json` for tooling that cannot import TypeScript — and names the one carrier of
346
+ the center's capability bits: `HEARTBEAT_REPLY_CAPABILITIES_KEY` (`"capabilities"`) on the heartbeat's 2xx reply.
307
347
  - **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
308
348
  carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side or
309
349
  a consumer's enforcement point). Today: `models[].compat` (1.10.0), `models[].headers` (6.4.0; the header-name
@@ -1 +1 @@
1
- 8.1.0
1
+ 8.2.0
@@ -0,0 +1,82 @@
1
+ {
2
+ "doc": {
3
+ "workers": [
4
+ {
5
+ "name": "alpha",
6
+ "enabled": true,
7
+ "host": "default",
8
+ "image": "sema-server:latest",
9
+ "secretRefs": {
10
+ "apiKeyEnv": "MODEL_KEY"
11
+ },
12
+ "runtime": {
13
+ "requirePrincipal": false,
14
+ "rateLimitRpm": 0,
15
+ "execution": {
16
+ "lane": "host",
17
+ "coords": {},
18
+ "secretRefs": {}
19
+ },
20
+ "store": {
21
+ "backend": "local",
22
+ "session": "auto"
23
+ }
24
+ },
25
+ "coords": {}
26
+ },
27
+ {
28
+ "name": "gamma",
29
+ "enabled": false,
30
+ "host": "default",
31
+ "image": "sema-server:latest",
32
+ "secretRefs": {
33
+ "apiKeyEnv": "MODEL_KEY"
34
+ },
35
+ "runtime": {
36
+ "requirePrincipal": false,
37
+ "rateLimitRpm": 0
38
+ },
39
+ "coords": {}
40
+ }
41
+ ]
42
+ },
43
+ "changed": [],
44
+ "dropped": [],
45
+ "refused": [
46
+ {
47
+ "name": "beta",
48
+ "index": 1,
49
+ "field": [
50
+ "env"
51
+ ],
52
+ "reason": "the stored worker is not a valid WorkerSpec — env: env cannot set reserved key \"FLEET_GENERATION\" — set it via (set by the config renderer from the worker's generation — not configurable)",
53
+ "original": {
54
+ "name": "beta",
55
+ "enabled": true,
56
+ "host": "default",
57
+ "image": "sema-server:latest",
58
+ "secretRefs": {
59
+ "apiKeyEnv": "MODEL_KEY"
60
+ },
61
+ "runtime": {
62
+ "requirePrincipal": false,
63
+ "rateLimitRpm": 0,
64
+ "execution": {
65
+ "lane": "host",
66
+ "coords": {},
67
+ "secretRefs": {}
68
+ },
69
+ "store": {
70
+ "backend": "local",
71
+ "session": "auto"
72
+ }
73
+ },
74
+ "coords": {},
75
+ "env": {
76
+ "FLEET_GENERATION": "7",
77
+ "LOG_LEVEL": "debug"
78
+ }
79
+ }
80
+ }
81
+ ]
82
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "workers": [
3
+ {
4
+ "name": "alpha",
5
+ "enabled": true,
6
+ "host": "default",
7
+ "image": "sema-server:latest",
8
+ "secretRefs": {
9
+ "apiKeyEnv": "MODEL_KEY"
10
+ },
11
+ "runtime": {
12
+ "requirePrincipal": false,
13
+ "rateLimitRpm": 0,
14
+ "execution": {
15
+ "lane": "host",
16
+ "coords": {},
17
+ "secretRefs": {}
18
+ },
19
+ "store": {
20
+ "backend": "local",
21
+ "session": "auto"
22
+ }
23
+ },
24
+ "coords": {}
25
+ },
26
+ {
27
+ "name": "beta",
28
+ "enabled": true,
29
+ "host": "default",
30
+ "image": "sema-server:latest",
31
+ "secretRefs": {
32
+ "apiKeyEnv": "MODEL_KEY"
33
+ },
34
+ "runtime": {
35
+ "requirePrincipal": false,
36
+ "rateLimitRpm": 0,
37
+ "execution": {
38
+ "lane": "host",
39
+ "coords": {},
40
+ "secretRefs": {}
41
+ },
42
+ "store": {
43
+ "backend": "local",
44
+ "session": "auto"
45
+ }
46
+ },
47
+ "coords": {},
48
+ "env": {
49
+ "FLEET_GENERATION": "7",
50
+ "LOG_LEVEL": "debug"
51
+ }
52
+ },
53
+ {
54
+ "name": "gamma",
55
+ "enabled": false,
56
+ "host": "default",
57
+ "image": "sema-server:latest",
58
+ "secretRefs": {
59
+ "apiKeyEnv": "MODEL_KEY"
60
+ },
61
+ "runtime": {
62
+ "requirePrincipal": false,
63
+ "rateLimitRpm": 0
64
+ },
65
+ "coords": {}
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "8.2.0 逐行终判:一份已是 7.x 形的 workers 文档(三行都不需要迁),其中 beta 在 env 里设了保留名 FLEET_GENERATION(渲染器是它的唯一写手)⇒ beta 进 refused(field = env,拒因是读侧自己那句,点名保留键与它的唯一写位),alpha / gamma 原样留在 doc,changed / dropped 为空;过滤后的 doc 整域读得进。8.1.0 及以前这份文档迁移回零变化、随后整域读抛。"
4
+ }
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { formatConfigPath, sanitizeZodError } from "./config-fns.js";
21
21
  import { MIGRATE_WORKERS_COMMAND } from "./execution-lane-index.js";
22
+ import { issueText } from "./issue-text.js";
22
23
  import { migrateWorkersDomain } from "./migrate-workers.js";
23
24
  import { DOMAIN_SCHEMAS } from "./types.js";
24
25
  const USAGE = [
@@ -69,11 +70,11 @@ function verdictOf(m) {
69
70
  refused: issues.map((i) => {
70
71
  const [head, index, ...rest] = i.path;
71
72
  if (head !== "workers" || typeof index !== "number")
72
- return { name: null, field: i.path, reason: i.message };
73
+ return { name: null, field: i.path, reason: issueText(i) };
73
74
  const entry = workers[index];
74
- return { name: isRecord(entry) && typeof entry.name === "string" ? entry.name : null, field: rest, reason: i.message };
75
+ return { name: isRecord(entry) && typeof entry.name === "string" ? entry.name : null, field: rest, reason: issueText(i) };
75
76
  }),
76
- why: `the migrated document does not read as a 7.0.0 workers domain — ${issues.map((i) => `${formatConfigPath(i.path)}: ${i.message}`).join("; ")}`,
77
+ why: `the migrated document does not read as a 7.0.0 workers domain — ${issues.map((i) => `${formatConfigPath(i.path)}: ${issueText(i)}`).join("; ")}`,
77
78
  };
78
79
  }
79
80
  const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -0,0 +1,14 @@
1
+ {
2
+ "FLEET_PATHS": {
3
+ "instances": "/api/fleet/instances",
4
+ "usage": "/api/fleet/usage",
5
+ "quotaLeases": "/api/fleet/leases",
6
+ "servingLease": "/api/fleet/live/serving-lease",
7
+ "hostPlanner": "/api/fleet/live/host-planner/{host}"
8
+ },
9
+ "FLEET_LIVE_PATHS": [
10
+ "/api/fleet/instances",
11
+ "/api/fleet/live/serving-lease",
12
+ "/api/fleet/live/host-planner/{host}"
13
+ ]
14
+ }