@sema-agent/settings-schema 8.0.0 → 8.1.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 +84 -0
- package/README.md +32 -1
- package/conformance/VERSION +1 -1
- package/dist/api/auth-bridge.d.ts +2 -2
- package/dist/fleet.d.ts +417 -3
- package/dist/fleet.js +287 -12
- package/dist/types.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,90 @@
|
|
|
6
6
|
> `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
|
|
7
7
|
> 包名是**历史记录**,如实保留不追改。
|
|
8
8
|
|
|
9
|
+
## 8.1.0 — 2026-10-08 — 引擎自铸信号的共享型:`RestartSignal.withheld` · `EngineSignal` · 服务租约 API 型与中心能力位 · 排空请求 / 回执型 · fleet env 名常量(全部 additive)
|
|
10
|
+
|
|
11
|
+
> **判档 = minor**(README「发版纪律」:新键 / 新导出 = minor)。本版只加**型与契约**,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement`
|
|
12
|
+
> 的形、`isReadyForTraffic` / `isRoutable` 的行为一字未动(**唯一一处校验方向的收紧**:`FLEET_GENERATION` 进保留 env 名表,见第五节——这个名字是本版才出现的,此前没有任何合法写手可被收紧,判档仍按 minor);`RestartSignal` 只多一个**可选**位(缺席 = 旧形,读出形不多铸键)。新增的是 `./fleet`
|
|
13
|
+
> (与主入口)上的 27 个导出名,导出面登记表里只多名字行(合计数字随之变),无删、无改形。
|
|
14
|
+
|
|
15
|
+
### 一、`RestartSignal.withheld?`(可选布尔,缺席 = false)
|
|
16
|
+
|
|
17
|
+
引擎自己判定「这一拍不该被换代」(需要重启,但此刻换掉这只实例会丢它手里的状态)。它是 `restart` **里面**的限定词,不是独立信号:只在 `restart` 在场时才有。
|
|
18
|
+
缺席与显式 `false` 同义;读器不替生产方铸这个键。`readRestartSignal` 原样读出,心跳 `InstanceAnnouncement.restart` 与 `/health` 两个出口仍是同一只 schema。
|
|
19
|
+
金样加一格带 `withheld` 的 `/health` 体(`test/restart-signal-6.1.0.test.ts`)。
|
|
20
|
+
|
|
21
|
+
### 二、`EngineSignal` + `readEngineSignal(health)`(+ `EngineDraining`)
|
|
22
|
+
|
|
23
|
+
- `EngineSignal` = 引擎自铸的那一只对象:`{ ready, draining?: { actionId, since, deadlineAt }, restart?, generation?, instanceId?, leaseUntil?, configTargetVersion?, configAppliedVersion? }`。
|
|
24
|
+
心跳与 `/health` 携带**同名键、同值**,键**平铺**在体上(没有包装键)。`restart` 引的就是 `RestartSignal` 那一只 schema,`draining` 的三键同时是排空回执的三键(`EngineDraining`)。
|
|
25
|
+
`generation` 是配置渲染器所设 env 的回声(缺席 ⇔ 非 fleet 成员),**规范十进制串**(见下条);**`instanceId` 是引擎每进程自铸的身份**,不读 env(同一个 Deployment 的副本共用一份 env、容器重启复用 env,env 给不了进程身份),
|
|
26
|
+
单机引擎上也在场——只有 `generation` 标记 fleet 成员。
|
|
27
|
+
- **与 `InstanceHealth` 的关系**:`InstanceHealth` 是 `/health` 的自描述(开放记录,`.passthrough()`,也装宿主自己加的 build / pid / port / 存储姿态);`EngineSignal` 是其中**引擎自铸**那一子集的**闭形**。
|
|
28
|
+
读时走默认 strip——整份 `/health` 体进去,只出这八个键。任一键形坏 ⇒ 整只信号 `undefined`(不拿剩下的充数)。
|
|
29
|
+
- **读口只有一只**:`readEngineSignal` 与 `readRestartSignal` / `readModelRefusals` 同走一个私有的受保护读口(原先只服务 `/health` 单键的那只,本版把「取哪个键」参数化、改名 `readSignalBody`;
|
|
30
|
+
行为不变)。同一份敌意输入,三个读器同一个结局。
|
|
31
|
+
- **方言**:`draining` 在 `InstanceHealth` / `InstanceAnnouncement` 上是**布尔**(8.0 方言,本版不动),在 `EngineSignal` 上是**对象**。布尔 `draining` 的体读不出 `EngineSignal`(`undefined`);
|
|
32
|
+
`undefined` 是「没有这只信号」,**永远不是「就绪」**——路由轴上的调用方应把它当不可路由,除非有意回落到布尔方言。
|
|
33
|
+
`isReadyForTraffic` / `isRoutable` 的输入留在布尔方言(`EngineSignal` 过不了它们的类型,这是故意的:`!== true` 会把对象形 `draining` 读成「就绪」);下一个 major 把布尔对(`draining` / `drainingSince`)退役、两个谓词改吃 `EngineSignal`。
|
|
34
|
+
|
|
35
|
+
### 三、服务租约 API 型
|
|
36
|
+
|
|
37
|
+
- `ServingLeaseRequest = { worker, scope?, generation, instanceId }`;
|
|
38
|
+
`ServingLeaseResponse` = 判别联合,**三词闭集** `granted { ttlMs }` / `denied { reason }` / `unknown`(`SERVING_LEASE_VERDICTS`);`denied` 的原因闭集恰两词 `superseded` | `retired`(`SERVING_LEASE_DENIED_REASONS`)。
|
|
39
|
+
- **`unknown` 不是 `denied` 的一种**:它不带 reason(带了也被剥掉,读不成 denied),表示中心此刻没有这只实例的生命周期对象(库恢复中、scope 迁移中、尚未认领)——对「该不该继续服务」什么都没说。
|
|
40
|
+
只有 `denied` 臂约束引擎停服,且**只凭正面证据**:中心对 (scope, worker) 持久化一只代际下限(永不回收),低于下限 ⇒ `superseded`;`retired` 同样要有中心持有的记录;**对象缺席 ⇒ `unknown`,永不拒**。
|
|
41
|
+
**读器按 `verdict` 键判**(两张脸,失败方向随含义):严格线形 `ServingLeaseResponse` 说的是中心**能发什么**(原因闭集恰两词);`readServingLeaseResponse` 返回 `ServingLeaseReading`——
|
|
42
|
+
`verdict: "denied"` 时 `reason` 不在闭集 / 缺 / 非串 ⇒ **仍是 `denied`**,`reason` 读成占位词 `SERVING_LEASE_UNKNOWN_REASON`(`"unknown_reason"`),引擎照样自排空,
|
|
43
|
+
不会因中心学会第三个原因词就把 `denied` 读成 `undefined` 而继续服务。占位词只在读出形里,生产方发不出它。判别词本身不在三词闭集、或 `granted` 的 `ttlMs` 不合 ⇒ `undefined`(读方按中心不可达处理,既不当 `denied` 也不当 `granted`)。
|
|
44
|
+
- **`generation` = 非负整数的规范十进制串**(`"0"`、`"7"`、`"10"`;不许 `"07"`、符号、小数、空白、非 ASCII 数字,也不许数字型),三处 wire 形(`EngineSignal.generation` / `ServingLeaseRequest.generation` / `DrainRequest.expectedGeneration`)同一条判据。
|
|
45
|
+
新导出 `isCanonicalGeneration(value)`(判别器,接 `unknown`)与 `compareGeneration(a, b)`(数值比,`"9"` 早于 `"10"`,位数不限;非规范入参响亮抛 `TypeError`)——中心持久化的代际下限靠它比较。
|
|
46
|
+
- **`granted` 带的是时长 `ttlMs`(正整数 ms),不是绝对时刻**:引擎按「发出请求时的本地单调钟 + `ttlMs`」自己算到期,中心按库时间记自己的到期,两边不比钟(`EngineSignal.leaseUntil` 是引擎据此算出的到期)。
|
|
47
|
+
- 常量:`SERVING_LEASE_TTL_DEFAULT_MS = 600_000`、`SERVING_LEASE_TTL_FLOOR_MS = 60_000`、`SERVING_LEASE_DRIFT_MARGIN_MS`(**派生自** `DEFAULT_HEARTBEAT_MS`,不是第二个数)。
|
|
48
|
+
配置的 TTL 过**一只**夹取函数 `clampServingLeaseTtlMs(n)`:读不出(缺 / `NaN` / 无穷 / 非数字)⇒ 缺省;低于地板(含 0 与负数)⇒ 地板;其余取整数 ms,无上限。它给**签发侧**的配置用;
|
|
49
|
+
应答里的 `ttlMs` 按授予值原样使用、**不**向上夹——引擎把短授予拉长到地板,等于持有租约超过中心记下的期限,那正是围栏要防的事。
|
|
50
|
+
- 读器:`readServingLeaseRequest`(中心侧)/ `readServingLeaseResponse`(引擎侧),同一只受保护读口。
|
|
51
|
+
- **中心能力位 `CenterCapabilities = { servingLease?: true }`** + `readCenterCapabilities(body)`:租约客户端**只在位在场时启用**;缺席(老中心 / 读不出)= 中心不支持租约 ⇒ 引擎不续租、也不被围栏,
|
|
52
|
+
所以执行器先于中心上线时,不会把还没有租约端点的中心逼成 `lease_expired`。位只有「`true` 或缺席」一种拼法(没有 `false`);未知位被剥,已知位的值不是 `true` ⇒ 整次读 `undefined`(读方等同缺席)。
|
|
53
|
+
这是兼容门、不是裁决——形里没有「中心拒绝租约」这句话。本仓原先没有中心能力位的表达,故自立这一只;承载它的中心端点不在本包定。
|
|
54
|
+
|
|
55
|
+
### 四、排空请求 / 回执型
|
|
56
|
+
|
|
57
|
+
- `DrainRequest = { actionId, scope?, worker, expectedGeneration?, instanceId?, deadlineAt, term? }`:身份(`scope?` / `worker` / `expectedGeneration?` / `instanceId?`)由排空口逐字段「缺席对缺席」比对——
|
|
58
|
+
非 fleet 成员没有 generation,故缺 `expectedGeneration` 是单机形而不是通配;`instanceId` 在场时是引擎自铸的进程身份。本形只说什么是合式请求,身份不符是排空口的拒因。
|
|
59
|
+
**两条车道,一个形**:Docker 车道与中心直调用这条 HTTP 排空请求;Kubernetes 车道不逐 pod 发 HTTP(寻址与 RBAC 不合),靠**租约拒 ⇒ 引擎自排空**。
|
|
60
|
+
- `DrainReceipt = EngineDraining.extend({ state: "draining" | "already_draining" })`(`DRAIN_RECEIPT_STATES`),与 `EngineSignal.draining` 同一只形,不抄一份。
|
|
61
|
+
- 读器:`readDrainRequest` / `readDrainReceipt`。
|
|
62
|
+
|
|
63
|
+
### 五、fleet env 名常量
|
|
64
|
+
|
|
65
|
+
`FLEET_GENERATION_ENV = "FLEET_GENERATION"`:配置渲染器设、引擎读回并回声成 `EngineSignal.generation`;**它进了保留 env 名表**(`RESERVED_WORKER_ENV`,`WorkerSpec.env` / `secretEnv` 设它即被拒,拒因指路「set by the config renderer … not configurable」),渲染器是唯一写手;两侧都 import 常量,不各自拼字符串
|
|
66
|
+
(整个 `src/` 里这个字面量只出现一次,有源码文本钉)。env 缺席 ⇔ 非 fleet 成员。**刻意没有实例身份的 env 名**——实例身份由引擎每进程自铸(见第二节),`src/` 里 `FLEET_INSTANCE_ID` 一次都不许出现(负钉)。
|
|
67
|
+
|
|
68
|
+
### 用户可见变化(谁受影响 / 怎么改 / 不改会怎样)
|
|
69
|
+
|
|
70
|
+
1. **只用 schema / 读写 helper、不碰 fleet 的消费方**:零变化。
|
|
71
|
+
2. **构造或解析 `RestartSignal` 的消费方**:可以多带一个可选布尔 `withheld`;不带照旧。**对 `RestartSignal` 的键做穷举**(`Record<keyof RestartSignal, …>`)的会编译红(多一个键)——按「提货成本」处理,补一行即可。
|
|
72
|
+
3. **要读引擎信号的消费方**(中心 / 壳 / 编排器):用 `readEngineSignal(health)`,不要再各自对 `health.ready` / `health.draining` 手写判断;`undefined` 当「没有信号」,不当「就绪」。
|
|
73
|
+
4. **仍读 `InstanceHealth.draining`(布尔)的**:不动。但注意:一旦某个引擎版本把 `/health` 的 `draining` 铸成对象(`EngineSignal` 方言),`InstanceHealth.safeParse` 对那份体会失败——那是该引擎版本与下一个 major 之间的接线问题,本版没有替它做兼容臂。
|
|
74
|
+
5. **实现租约客户端 / 排空口的引擎(server)与中心(管理面)**:用 `ServingLeaseRequest` / `ServingLeaseResponse` / `DrainRequest` / `DrainReceipt` / `CenterCapabilities` 与 `FLEET_GENERATION_ENV` 对齐形;
|
|
75
|
+
租约客户端只在 `readCenterCapabilities(...)?.servingLease === true` 时启用;`instanceId` 自己每进程铸一个,不读 env;对 `verdict` 做 `switch` 时加 `default: never` 臂,以后加词编译会红;
|
|
76
|
+
`granted` 用 `ttlMs`(时长)按发请求时的本地单调钟算到期;应答一律走 `readServingLeaseResponse`(按 `verdict` 键判,`denied` 的原因词读不出也照样排空);`generation` 一律写规范十进制串(`"7"`),用 `compareGeneration` 比大小。
|
|
77
|
+
6. **在 `WorkerSpec.env` / `secretEnv` 里设了 `FLEET_GENERATION` 的**:现在被拒(保留名,拒因指路「set by the config renderer … not configurable」)。这个名字是本版才出现的,正常部署没人设过;
|
|
78
|
+
设过的删掉那一行,由渲染器按该 worker 的代际写。**持有 `RESERVED_WORKER_ENV` 总数断言的测试**(本仓的历史钉就是一例)要 +1。
|
|
79
|
+
|
|
80
|
+
### 红先 / 门
|
|
81
|
+
|
|
82
|
+
- 先写 `test/engine-signal-8.1.0.test.ts`(含 `restart-signal-6.1.0` 的金样格)对着 8.0.0 的 `src/fleet.ts` 跑:新增 31 格里 25 格红(新导出缺席、`withheld` 被剥掉),
|
|
83
|
+
另 6 格是 additive 反向格(「不带 withheld 的旧形读出形不铸键」「`InstanceHealth` 四键 / passthrough 照旧」「就绪谓词行为照旧」等),在 8.0.0 上本来就绿;
|
|
84
|
+
逐步实现后同文件全绿(含 3 格源码文本钉:读口只有一只、漂移余量的定义、env 字面量单点与实例身份无 env 名)。
|
|
85
|
+
之后两轮增补(第一轮:删实例身份 env 名常量、加中心能力位、两处注;第二轮:`ttlMs`、规范 `generation`、读器按 `verdict` 键判、保留 env 名与夹取函数)里,
|
|
86
|
+
除中心能力位(源码先写、事后用变异自证补咬合)外,新格都先红再绿;`test/migration-signpost-7.0.1.test.ts` 里「保留名总数 = 90」的历史钉改成「90 + 登记的增词」(增词表里目前只有 `FLEET_GENERATION`)。
|
|
87
|
+
- 变异自证(29 个变异,每个都至少使一格红,红的是预期的那一格):`withheld` 改 `.default(false)`、`EngineSignal` 改 `.passthrough()`、`unknown` 臂加 reason、`denied` 的 reason 改可选、漂移余量改字面量、`DrainReceipt` 改成抄一份、`EngineSignal.restart` 改成第二份形、`readEngineSignal` 自带一份 try/catch、`ready` 改可选、`draining` 兼收布尔、原因 / 判别词表多一词、env 名改字或在别处再拼一份、实例身份 env 名常量回来、能力位可写 `false`、能力位自带读口 / 严判未知位、`ttlMs` 变可选、`generation` 放宽成任意非空串 / 规范串正则放过前导零、`compareGeneration` 改字典序 / 不再抛、`denied` 读脸对 reason 严判、占位词混进闭集、`FLEET_GENERATION` 从保留表摘掉、`clampServingLeaseTtlMs` 去地板 / 把读不出夹到地板等。
|
|
88
|
+
- 导出面登记表(`docs/API-SURFACE.md`)重生成:`.` 与 `./fleet` 相对 8.0.0 各 +27 名,无删无改形。
|
|
89
|
+
|
|
90
|
+
### 未闭环(如实)
|
|
91
|
+
|
|
92
|
+
|
|
9
93
|
## 8.0.0 — 2026-10-04 — 一致性门第四条腿 `strictWrite`(严写受理)+ 每条腿都要表态(BREAKING,kit 类型面与 release 律)· `migrate-workers --check` 与真跑过同一只判定(BREAKING,CLI 退出码 / `refused[]`)· 导出面登记表机器门
|
|
10
94
|
|
|
11
95
|
> **判档 = major**(README「发版纪律」):kit 的闭集 `LegName` 加一员、`ConformanceAdapter.legs` 从 `Partial<Record<…>>` 改成全量
|
package/README.md
CHANGED
|
@@ -48,6 +48,24 @@ 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.0.0 → 8.1.0(全部 additive,零动作可升)
|
|
52
|
+
|
|
53
|
+
8.1.0 只在 `./fleet`(与主入口)上**加**型与契约,零行为:既有字段的语义、`InstanceHealth` / `InstanceAnnouncement` 的形、两个就绪谓词一字未动。新增清单:
|
|
54
|
+
|
|
55
|
+
- `RestartSignal.withheld?` —— 可选布尔,引擎自判「这一拍不该被换代」;缺席 = `false`,读出形不铸键。
|
|
56
|
+
- `EngineSignal`(+ `EngineDraining`)与 `readEngineSignal(health)` —— 引擎自铸信号的**闭形**(`ready` 必填,`draining?` / `restart?` / `generation?` / `instanceId?` /
|
|
57
|
+
`leaseUntil?` / `configTargetVersion?` / `configAppliedVersion?` 可选),键平铺在 `/health` 与心跳体上;与既有读器走同一只受保护读口。
|
|
58
|
+
`undefined` = 没有这只信号,**不是就绪**;布尔 `draining` 的旧方言体读不出它(两个就绪谓词的输入仍是布尔方言,下一个 major 收)。
|
|
59
|
+
- 代际 —— `generation` 是非负整数的**规范十进制串**(`"7"`,不是 `"07"`、不是数字型),判别器 `isCanonicalGeneration`、数值比较 `compareGeneration`;三处 wire 形共用这一条判据。
|
|
60
|
+
- 服务租约 API 型 —— `ServingLeaseRequest`、`ServingLeaseResponse`(`granted { ttlMs }` / `denied { reason: superseded | retired }` / `unknown` 三词闭集,
|
|
61
|
+
**`unknown` 不是 `denied` 的一种**,`denied` 只凭正面证据;读器按 `verdict` 键判,`denied` 的原因词读不出时仍是 `denied`,原因读成 `unknown_reason`)、`SERVING_LEASE_TTL_DEFAULT_MS` / `SERVING_LEASE_TTL_FLOOR_MS` / `SERVING_LEASE_DRIFT_MARGIN_MS`、读器 `readServingLeaseRequest` / `readServingLeaseResponse`;
|
|
62
|
+
中心能力位 `CenterCapabilities { servingLease?: true }` + `readCenterCapabilities` —— 租约客户端只在位在场时启用,缺席 = 中心不支持租约(不续租、不围栏)。
|
|
63
|
+
- 排空口型 —— `DrainRequest`、`DrainReceipt`(`state: draining | already_draining`)、读器 `readDrainRequest` / `readDrainReceipt`。
|
|
64
|
+
- `FLEET_GENERATION_ENV` —— 配置渲染器与引擎共用的 generation env 名常量(实例身份 `instanceId` 由引擎每进程自铸,没有 env 名);它同时进了 `RESERVED_WORKER_ENV`,`worker.env` / `secretEnv` 设不了它。
|
|
65
|
+
- `clampServingLeaseTtlMs(n)` —— 配置的租约 TTL 的唯一夹取点(读不出 ⇒ 缺省 600 000;低于地板 ⇒ 60 000;无上限)。
|
|
66
|
+
|
|
67
|
+
提货成本只有一处:对 `RestartSignal` 的键做穷举的消费方会因多一个键编译红。逐条(谁受影响 / 怎么改)见 CHANGELOG 8.1.0。
|
|
68
|
+
|
|
51
69
|
## 升级指引:7.x → 8.0.0
|
|
52
70
|
|
|
53
71
|
8.0.0 的破坏面只在**一致性门 kit**(`./conformance`)的类型与 release 律、以及 CLI `migrate-workers --check` 的一种结局上;契约 schema、域默认值、
|
|
@@ -272,7 +290,20 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
272
290
|
sends it (UNKNOWN, not "none"). A name that cannot ride out is left out by the engine, judged by ONE predicate,
|
|
273
291
|
`isReportableRefusalName` (the wire can carry the key — `__proto__` it cannot — and `isDisclosableIdentity`, the
|
|
274
292
|
rule the dropped-entry identity on read warnings already uses: the `DISCLOSABLE_IDENTITY_RE` allowlist ∧ not
|
|
275
|
-
secret-shaped, 6.3.0).
|
|
293
|
+
secret-shaped, 6.3.0). **8.1.0** (additive) puts the engine's own state in one closed shape: `RestartSignal` gains the
|
|
294
|
+
optional `withheld` ("the engine says this beat must not be handed to a new generation"; absent ≡ false);
|
|
295
|
+
`EngineSignal` (`{ ready, draining?: EngineDraining, restart?, generation?, instanceId?, leaseUntil?,
|
|
296
|
+
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
|
|
298
|
+
`readEngineSignal(health)` reads it through the same guarded reader as `readRestartSignal` / `readModelRefusals` —
|
|
299
|
+
`undefined` means "no such signal", never "ready", and a body carrying the boolean `draining` of the 8.0 dialect is not
|
|
300
|
+
one. The serving-lease wire (`ServingLeaseRequest` / `ServingLeaseResponse`: `granted { ttlMs }` | `denied { reason:
|
|
301
|
+
superseded | retired }` | `unknown` — a closed three-word set in which `unknown` is NOT a kind of `denied`; plus
|
|
302
|
+
`SERVING_LEASE_TTL_DEFAULT_MS` 600 000, `SERVING_LEASE_TTL_FLOOR_MS` 60 000 and `SERVING_LEASE_DRIFT_MARGIN_MS`, derived
|
|
303
|
+
from `DEFAULT_HEARTBEAT_MS`; `denied` rests on positive evidence only), the center capability bit `CenterCapabilities`
|
|
304
|
+
(`servingLease?: true` — absent means the center has no lease, so the engine neither renews nor is fenced), the drain
|
|
305
|
+
endpoint's `DrainRequest` / `DrainReceipt`, one reader per wire shape, and the env name `FLEET_GENERATION_ENV` are defined
|
|
306
|
+
here once for the renderer and the engine (an instance's id is minted by the engine per process — it has no env name).
|
|
276
307
|
- **Declared-but-unvalidated keys** (`PARSE_TRANSPARENT_KEYS`, 2.0.0): the ONE registry of keys this package
|
|
277
308
|
carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side or
|
|
278
309
|
a consumer's enforcement point). Today: `models[].compat` (1.10.0), `models[].headers` (6.4.0; the header-name
|
package/conformance/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.1.0
|
|
@@ -265,14 +265,14 @@ export declare const VerifiedIdentity: z.ZodObject<{
|
|
|
265
265
|
scope: z.ZodOptional<z.ZodString>;
|
|
266
266
|
}, "strip", z.ZodTypeAny, {
|
|
267
267
|
principal: string;
|
|
268
|
+
scope?: string | undefined;
|
|
268
269
|
role?: string | undefined;
|
|
269
270
|
grantId?: string | undefined;
|
|
270
|
-
scope?: string | undefined;
|
|
271
271
|
}, {
|
|
272
272
|
principal: string;
|
|
273
|
+
scope?: string | undefined;
|
|
273
274
|
role?: string | undefined;
|
|
274
275
|
grantId?: string | undefined;
|
|
275
|
-
scope?: string | undefined;
|
|
276
276
|
}>;
|
|
277
277
|
export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
|
|
278
278
|
/** The verifier's total result — exactly one of ok/error. */
|
package/dist/fleet.d.ts
CHANGED
|
@@ -51,7 +51,8 @@ export declare function drainVerdict(s: DrainSnapshot): DrainVerdict;
|
|
|
51
51
|
* {@link readRestartSignal} — {@link InstanceHealth} keeps carrying it untyped, see there). One mint point, two exits —
|
|
52
52
|
* never a second judgement.
|
|
53
53
|
*
|
|
54
|
-
* Shape = what the server already ships on /health (`restart: { reasons, version, since }
|
|
54
|
+
* Shape = what the server already ships on /health (`restart: { reasons, version, since }`, plus the optional `withheld`
|
|
55
|
+
* qualifier below). PRESENCE is the signal:
|
|
55
56
|
* present ⇔ a restart is required, absent ⇔ not; there is deliberately no `required` boolean inside (a boolean next
|
|
56
57
|
* to presence is redundant, and two spellings of one fact drift).
|
|
57
58
|
* - `reasons` — which restart-to-apply slices differ from the value baked at boot (the vocabulary is the engine's;
|
|
@@ -60,19 +61,26 @@ export declare function drainVerdict(s: DrainSnapshot): DrainVerdict;
|
|
|
60
61
|
* later publishes, so a "one restart per version" dedupe bounds restarts per config version, not per reason set).
|
|
61
62
|
* Judged exactly as `readEffectiveWire` judges a config version (a finite number) — one quantity, one rule.
|
|
62
63
|
* - `since` — epoch ms when this restart first became required (stable across refreshes while `reasons` holds).
|
|
64
|
+
* - `withheld` — optional: the ENGINE's own verdict that this beat must not be handed to a new generation ("a restart is
|
|
65
|
+
* required, but replacing this instance right now would lose state it holds"). Absent ≡ `false` — an explicit `false`
|
|
66
|
+
* on the wire reads the same as the key being absent, and a reader never mints the key when the producer did not send
|
|
67
|
+
* it. It is a qualifier INSIDE the restart signal, never a signal of its own: it exists only where `restart` does.
|
|
63
68
|
*/
|
|
64
69
|
export declare const RestartSignal: z.ZodObject<{
|
|
65
70
|
reasons: z.ZodArray<z.ZodString, "many">;
|
|
66
71
|
version: z.ZodNumber;
|
|
67
72
|
since: z.ZodNumber;
|
|
73
|
+
withheld: z.ZodOptional<z.ZodBoolean>;
|
|
68
74
|
}, "strip", z.ZodTypeAny, {
|
|
69
75
|
reasons: string[];
|
|
70
76
|
version: number;
|
|
71
77
|
since: number;
|
|
78
|
+
withheld?: boolean | undefined;
|
|
72
79
|
}, {
|
|
73
80
|
reasons: string[];
|
|
74
81
|
version: number;
|
|
75
82
|
since: number;
|
|
83
|
+
withheld?: boolean | undefined;
|
|
76
84
|
}>;
|
|
77
85
|
export type RestartSignal = z.infer<typeof RestartSignal>;
|
|
78
86
|
/**
|
|
@@ -211,7 +219,11 @@ export declare const InstanceHealth: z.ZodObject<{
|
|
|
211
219
|
drainingSince: z.ZodOptional<z.ZodNumber>;
|
|
212
220
|
}, z.ZodTypeAny, "passthrough">>;
|
|
213
221
|
export type InstanceHealth = z.infer<typeof InstanceHealth>;
|
|
214
|
-
/** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch).
|
|
222
|
+
/** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch).
|
|
223
|
+
* Its input is the boolean-`draining` dialect of {@link InstanceHealth}. An {@link EngineSignal} types `draining` as an
|
|
224
|
+
* object, so it does not typecheck here (on purpose: `!== true` would read an object-valued `draining` as "ready").
|
|
225
|
+
* The next major retires the boolean pair (`draining` / `drainingSince`) and gives both this predicate and
|
|
226
|
+
* {@link isRoutable} `EngineSignal` as their input. */
|
|
215
227
|
export declare function isReadyForTraffic(h: Pick<InstanceHealth, "draining">): boolean;
|
|
216
228
|
/** Heartbeat cadence the announcement declares (center judges staleness against the DECLARED cadence, so a
|
|
217
229
|
* slow-heartbeat worker is not falsely reaped). */
|
|
@@ -242,14 +254,17 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
242
254
|
reasons: z.ZodArray<z.ZodString, "many">;
|
|
243
255
|
version: z.ZodNumber;
|
|
244
256
|
since: z.ZodNumber;
|
|
257
|
+
withheld: z.ZodOptional<z.ZodBoolean>;
|
|
245
258
|
}, "strip", z.ZodTypeAny, {
|
|
246
259
|
reasons: string[];
|
|
247
260
|
version: number;
|
|
248
261
|
since: number;
|
|
262
|
+
withheld?: boolean | undefined;
|
|
249
263
|
}, {
|
|
250
264
|
reasons: string[];
|
|
251
265
|
version: number;
|
|
252
266
|
since: number;
|
|
267
|
+
withheld?: boolean | undefined;
|
|
253
268
|
}>>;
|
|
254
269
|
/** S-768: present ⇔ this instance has at least one reportable model refusal — the SAME {@link ModelRefusalsWire} /health
|
|
255
270
|
* carries, minted by the same getter; read it with {@link readModelRefusals}`(announcement)` (never off this field
|
|
@@ -289,6 +304,7 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
289
304
|
reasons: string[];
|
|
290
305
|
version: number;
|
|
291
306
|
since: number;
|
|
307
|
+
withheld?: boolean | undefined;
|
|
292
308
|
} | undefined;
|
|
293
309
|
modelRefusals?: {
|
|
294
310
|
entries: Record<string, string>;
|
|
@@ -305,6 +321,7 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
305
321
|
reasons: string[];
|
|
306
322
|
version: number;
|
|
307
323
|
since: number;
|
|
324
|
+
withheld?: boolean | undefined;
|
|
308
325
|
} | undefined;
|
|
309
326
|
modelRefusals?: unknown;
|
|
310
327
|
heartbeatMs?: number | undefined;
|
|
@@ -346,14 +363,17 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
346
363
|
reasons: z.ZodArray<z.ZodString, "many">;
|
|
347
364
|
version: z.ZodNumber;
|
|
348
365
|
since: z.ZodNumber;
|
|
366
|
+
withheld: z.ZodOptional<z.ZodBoolean>;
|
|
349
367
|
}, "strip", z.ZodTypeAny, {
|
|
350
368
|
reasons: string[];
|
|
351
369
|
version: number;
|
|
352
370
|
since: number;
|
|
371
|
+
withheld?: boolean | undefined;
|
|
353
372
|
}, {
|
|
354
373
|
reasons: string[];
|
|
355
374
|
version: number;
|
|
356
375
|
since: number;
|
|
376
|
+
withheld?: boolean | undefined;
|
|
357
377
|
}>>;
|
|
358
378
|
/** S-768: present ⇔ this instance has at least one reportable model refusal — the SAME {@link ModelRefusalsWire} /health
|
|
359
379
|
* carries, minted by the same getter; read it with {@link readModelRefusals}`(announcement)` (never off this field
|
|
@@ -399,6 +419,7 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
399
419
|
reasons: string[];
|
|
400
420
|
version: number;
|
|
401
421
|
since: number;
|
|
422
|
+
withheld?: boolean | undefined;
|
|
402
423
|
} | undefined;
|
|
403
424
|
modelRefusals?: {
|
|
404
425
|
entries: Record<string, string>;
|
|
@@ -418,6 +439,7 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
418
439
|
reasons: string[];
|
|
419
440
|
version: number;
|
|
420
441
|
since: number;
|
|
442
|
+
withheld?: boolean | undefined;
|
|
421
443
|
} | undefined;
|
|
422
444
|
modelRefusals?: unknown;
|
|
423
445
|
heartbeatMs?: number | undefined;
|
|
@@ -427,9 +449,400 @@ export type InstanceRegistration = z.infer<typeof InstanceRegistration>;
|
|
|
427
449
|
/** Silent past the declared cadence × multiplier ⇒ stale (auto-removed from the routable set). */
|
|
428
450
|
export declare function isInstanceStale(r: Pick<InstanceRegistration, "lastSeenMs" | "heartbeatMs">, nowMs: number): boolean;
|
|
429
451
|
/** Health fusion: routable ⇔ self-declared ready ∧ not draining ∧ not stale ∧ observer does not veto.
|
|
430
|
-
* Two sources merge as an AND — EITHER source's "not ready" removes the instance (fail-closed).
|
|
452
|
+
* Two sources merge as an AND — EITHER source's "not ready" removes the instance (fail-closed).
|
|
453
|
+
* Its input is the stored heartbeat row (boolean `draining`); it moves to {@link EngineSignal} in the same major as
|
|
454
|
+
* {@link isReadyForTraffic}. */
|
|
431
455
|
export declare function isRoutable(r: InstanceRegistration, nowMs: number): boolean;
|
|
432
456
|
export declare function routableInstances(rs: InstanceRegistration[], nowMs: number): InstanceRegistration[];
|
|
457
|
+
/**
|
|
458
|
+
* The engine's draining fact — ONE object wherever the engine says "I am draining": inside {@link EngineSignal}`.draining`
|
|
459
|
+
* and as the body of a {@link DrainReceipt}.
|
|
460
|
+
* - `actionId` — the action that put the engine into draining (idempotency key of the drain request).
|
|
461
|
+
* - `since` — epoch ms when draining began; it does not move when the same action repeats.
|
|
462
|
+
* - `deadlineAt` — epoch ms by which the engine stops on its own.
|
|
463
|
+
*/
|
|
464
|
+
export declare const EngineDraining: z.ZodObject<{
|
|
465
|
+
actionId: z.ZodString;
|
|
466
|
+
since: z.ZodNumber;
|
|
467
|
+
deadlineAt: z.ZodNumber;
|
|
468
|
+
}, "strip", z.ZodTypeAny, {
|
|
469
|
+
since: number;
|
|
470
|
+
actionId: string;
|
|
471
|
+
deadlineAt: number;
|
|
472
|
+
}, {
|
|
473
|
+
since: number;
|
|
474
|
+
actionId: string;
|
|
475
|
+
deadlineAt: number;
|
|
476
|
+
}>;
|
|
477
|
+
export type EngineDraining = z.infer<typeof EngineDraining>;
|
|
478
|
+
export declare function isCanonicalGeneration(value: unknown): value is string;
|
|
479
|
+
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
480
|
+
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
481
|
+
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
482
|
+
* first — the wire shapes of this file already do. */
|
|
483
|
+
export declare function compareGeneration(a: string, b: string): -1 | 0 | 1;
|
|
484
|
+
/**
|
|
485
|
+
* Everything the ENGINE itself mints about its own state, as one object — the closed form of the engine-owned subset of the
|
|
486
|
+
* /health self-description ({@link InstanceHealth} is that self-description: an open record, `.passthrough()`, which also
|
|
487
|
+
* carries what the host adds — build, pid, port, store posture). One mint point; the heartbeat and `/health` carry the same
|
|
488
|
+
* keys with the same values, flat (`ready`, `draining`, `restart`, … sit directly on the body, no wrapper key), and
|
|
489
|
+
* {@link readEngineSignal} is the one reader of either.
|
|
490
|
+
*
|
|
491
|
+
* - `ready` — always said (never "absent while ready"): the engine can take new work right now.
|
|
492
|
+
* - `draining` — present ⇔ draining ({@link EngineDraining}). NOTE the dialect: {@link InstanceHealth}`.draining` and
|
|
493
|
+
* {@link InstanceAnnouncement}`.draining` are BOOLEANS (the 8.0 dialect, kept untouched); here it is an object. A body
|
|
494
|
+
* whose `draining` is a boolean is therefore not an `EngineSignal` and reads as `undefined`.
|
|
495
|
+
* - `restart` — the one {@link RestartSignal}, same schema object, no copy.
|
|
496
|
+
* - `generation` — echo of the env the config renderer set on this instance ({@link FLEET_GENERATION_ENV}), a canonical
|
|
497
|
+
* decimal string ({@link isCanonicalGeneration}). Absent ⇔ not a fleet member (a single-machine engine): it takes no lease
|
|
498
|
+
* and is not fenced.
|
|
499
|
+
* - `instanceId` — the engine's OWN process identity, minted by the engine every time the process starts (never read from
|
|
500
|
+
* env: replicas of one Deployment share one env, and a restarted container reuses its env, so env cannot tell two
|
|
501
|
+
* processes apart). It is present on a standalone engine too — only `generation` marks fleet membership.
|
|
502
|
+
* - `leaseUntil` — epoch ms until which the engine holds its serving lease, as the engine computed it from the last
|
|
503
|
+
* `ttlMs` it was granted ({@link ServingLeaseResponse}); absent when it holds none.
|
|
504
|
+
* - `configTargetVersion` / `configAppliedVersion` — the config-center version the engine was told to converge on, and the
|
|
505
|
+
* one it has applied (opaque strings; equal ⇔ converged).
|
|
506
|
+
*
|
|
507
|
+
* Reading: `EngineSignal` is parsed with the default strip — a whole /health body goes in, only these keys come out. A
|
|
508
|
+
* shape violation in ANY key makes the whole signal `undefined` (it is never "the rest of it"). `undefined` means "no
|
|
509
|
+
* signal this reader can say" — an engine that predates the keys, or a malformed body — and is NEVER "ready": a caller on
|
|
510
|
+
* the routing axis treats it as not routable unless it deliberately falls back to the boolean dialect of
|
|
511
|
+
* {@link InstanceHealth}.
|
|
512
|
+
*/
|
|
513
|
+
export declare const EngineSignal: z.ZodObject<{
|
|
514
|
+
ready: z.ZodBoolean;
|
|
515
|
+
draining: z.ZodOptional<z.ZodObject<{
|
|
516
|
+
actionId: z.ZodString;
|
|
517
|
+
since: z.ZodNumber;
|
|
518
|
+
deadlineAt: z.ZodNumber;
|
|
519
|
+
}, "strip", z.ZodTypeAny, {
|
|
520
|
+
since: number;
|
|
521
|
+
actionId: string;
|
|
522
|
+
deadlineAt: number;
|
|
523
|
+
}, {
|
|
524
|
+
since: number;
|
|
525
|
+
actionId: string;
|
|
526
|
+
deadlineAt: number;
|
|
527
|
+
}>>;
|
|
528
|
+
restart: z.ZodOptional<z.ZodObject<{
|
|
529
|
+
reasons: z.ZodArray<z.ZodString, "many">;
|
|
530
|
+
version: z.ZodNumber;
|
|
531
|
+
since: z.ZodNumber;
|
|
532
|
+
withheld: z.ZodOptional<z.ZodBoolean>;
|
|
533
|
+
}, "strip", z.ZodTypeAny, {
|
|
534
|
+
reasons: string[];
|
|
535
|
+
version: number;
|
|
536
|
+
since: number;
|
|
537
|
+
withheld?: boolean | undefined;
|
|
538
|
+
}, {
|
|
539
|
+
reasons: string[];
|
|
540
|
+
version: number;
|
|
541
|
+
since: number;
|
|
542
|
+
withheld?: boolean | undefined;
|
|
543
|
+
}>>;
|
|
544
|
+
generation: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
545
|
+
instanceId: z.ZodOptional<z.ZodString>;
|
|
546
|
+
leaseUntil: z.ZodOptional<z.ZodNumber>;
|
|
547
|
+
configTargetVersion: z.ZodOptional<z.ZodString>;
|
|
548
|
+
configAppliedVersion: z.ZodOptional<z.ZodString>;
|
|
549
|
+
}, "strip", z.ZodTypeAny, {
|
|
550
|
+
ready: boolean;
|
|
551
|
+
draining?: {
|
|
552
|
+
since: number;
|
|
553
|
+
actionId: string;
|
|
554
|
+
deadlineAt: number;
|
|
555
|
+
} | undefined;
|
|
556
|
+
restart?: {
|
|
557
|
+
reasons: string[];
|
|
558
|
+
version: number;
|
|
559
|
+
since: number;
|
|
560
|
+
withheld?: boolean | undefined;
|
|
561
|
+
} | undefined;
|
|
562
|
+
instanceId?: string | undefined;
|
|
563
|
+
generation?: string | undefined;
|
|
564
|
+
leaseUntil?: number | undefined;
|
|
565
|
+
configTargetVersion?: string | undefined;
|
|
566
|
+
configAppliedVersion?: string | undefined;
|
|
567
|
+
}, {
|
|
568
|
+
ready: boolean;
|
|
569
|
+
draining?: {
|
|
570
|
+
since: number;
|
|
571
|
+
actionId: string;
|
|
572
|
+
deadlineAt: number;
|
|
573
|
+
} | undefined;
|
|
574
|
+
restart?: {
|
|
575
|
+
reasons: string[];
|
|
576
|
+
version: number;
|
|
577
|
+
since: number;
|
|
578
|
+
withheld?: boolean | undefined;
|
|
579
|
+
} | undefined;
|
|
580
|
+
instanceId?: string | undefined;
|
|
581
|
+
generation?: string | undefined;
|
|
582
|
+
leaseUntil?: number | undefined;
|
|
583
|
+
configTargetVersion?: string | undefined;
|
|
584
|
+
configAppliedVersion?: string | undefined;
|
|
585
|
+
}>;
|
|
586
|
+
export type EngineSignal = z.infer<typeof EngineSignal>;
|
|
587
|
+
/**
|
|
588
|
+
* The reader of {@link EngineSignal}, for either exit (a /health body or a heartbeat body — the keys are flat on both): the
|
|
589
|
+
* same guarded body reader as {@link readRestartSignal} and {@link readModelRefusals}, so the same odd input gets the same
|
|
590
|
+
* answer from all three. Shape mismatch, a missing `ready`, a non-object body, even a throwing accessor ⇒ `undefined` —
|
|
591
|
+
* never throws. See {@link EngineSignal} for what `undefined` means (not "ready").
|
|
592
|
+
*/
|
|
593
|
+
export declare function readEngineSignal(health: unknown): EngineSignal | undefined;
|
|
594
|
+
/**
|
|
595
|
+
* The env name a fleet member's generation travels under — the config renderer sets it, the engine reads it back and echoes
|
|
596
|
+
* it as {@link EngineSignal}`.generation`. Both sides import THIS constant; neither spells the name. The value is the
|
|
597
|
+
* generation this instance belongs to (opaque string); absent from the engine's env ⇔ not a fleet member (the standalone
|
|
598
|
+
* form). There is deliberately NO env name for the instance id: an instance's identity is minted by the engine per process
|
|
599
|
+
* ({@link EngineSignal}`.instanceId`), and env cannot carry it (see there).
|
|
600
|
+
*/
|
|
601
|
+
export declare const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
602
|
+
/**
|
|
603
|
+
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
604
|
+
* engine renews it with the center; the center answers per the lifecycle object it holds for that instance. Three constants
|
|
605
|
+
* fix the clock, all in ms:
|
|
606
|
+
* - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
|
|
607
|
+
* - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
|
|
608
|
+
* the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
|
|
609
|
+
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the center's clock-skew allowance: it adds this to a lease's recorded expiry
|
|
610
|
+
* before it counts that lease as over. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a
|
|
611
|
+
* second constant that could drift from it.
|
|
612
|
+
*/
|
|
613
|
+
export declare const SERVING_LEASE_TTL_DEFAULT_MS = 600000;
|
|
614
|
+
export declare const SERVING_LEASE_TTL_FLOOR_MS = 60000;
|
|
615
|
+
export declare const SERVING_LEASE_DRIFT_MARGIN_MS = 30000;
|
|
616
|
+
/**
|
|
617
|
+
* The ONE place a configured lease TTL becomes a usable one: not a number (absent, `NaN`, infinite, another type) ⇒
|
|
618
|
+
* {@link SERVING_LEASE_TTL_DEFAULT_MS}; a number below {@link SERVING_LEASE_TTL_FLOOR_MS} (zero and negatives included) ⇒ the
|
|
619
|
+
* floor; otherwise the number, truncated to whole ms, with no ceiling. It is for the ISSUING side's configuration. A reply's
|
|
620
|
+
* `ttlMs` is used exactly as granted and is never clamped upward — an engine that stretched a short grant to the floor would
|
|
621
|
+
* hold the lease past what the center recorded, which is the one thing the fence exists to prevent.
|
|
622
|
+
*/
|
|
623
|
+
export declare function clampServingLeaseTtlMs(configuredMs: number | undefined): number;
|
|
624
|
+
/**
|
|
625
|
+
* Engine → center: "may this instance keep serving?" The identity triple names the instance exactly — a lease is never
|
|
626
|
+
* asked for by worker name alone. `generation` is the env echo ({@link FLEET_GENERATION_ENV}); `instanceId` is the engine's
|
|
627
|
+
* own per-process identity ({@link EngineSignal}`.instanceId`), so a restarted process is a NEW instance to the center.
|
|
628
|
+
* `scope` is the tenant scope the worker lives in (absent = the single-scope deployment). Only fleet members ask: an engine
|
|
629
|
+
* without a generation (the standalone form) has no lease and sends nothing.
|
|
630
|
+
*/
|
|
631
|
+
export declare const ServingLeaseRequest: z.ZodObject<{
|
|
632
|
+
worker: z.ZodString;
|
|
633
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
634
|
+
generation: z.ZodEffects<z.ZodString, string, string>;
|
|
635
|
+
instanceId: z.ZodString;
|
|
636
|
+
}, "strip", z.ZodTypeAny, {
|
|
637
|
+
instanceId: string;
|
|
638
|
+
worker: string;
|
|
639
|
+
generation: string;
|
|
640
|
+
scope?: string | undefined;
|
|
641
|
+
}, {
|
|
642
|
+
instanceId: string;
|
|
643
|
+
worker: string;
|
|
644
|
+
generation: string;
|
|
645
|
+
scope?: string | undefined;
|
|
646
|
+
}>;
|
|
647
|
+
export type ServingLeaseRequest = z.infer<typeof ServingLeaseRequest>;
|
|
648
|
+
/**
|
|
649
|
+
* Why a lease is refused — a CLOSED set of exactly two words, owned here:
|
|
650
|
+
* - `superseded` — a newer generation of this worker has taken over; this instance is the old one.
|
|
651
|
+
* - `retired` — the center has retired this instance (or the worker it belongs to).
|
|
652
|
+
* Only {@link ServingLeaseResponse}'s `denied` arm carries a reason; `unknown` has none, by shape.
|
|
653
|
+
*/
|
|
654
|
+
export declare const SERVING_LEASE_DENIED_REASONS: readonly ["superseded", "retired"];
|
|
655
|
+
export declare const ServingLeaseDeniedReason: z.ZodEnum<["superseded", "retired"]>;
|
|
656
|
+
export type ServingLeaseDeniedReason = z.infer<typeof ServingLeaseDeniedReason>;
|
|
657
|
+
/**
|
|
658
|
+
* Center → engine: the verdict, a CLOSED set of three words ({@link SERVING_LEASE_VERDICTS}) — the arms of one
|
|
659
|
+
* discriminated union, so a reader that switches on `verdict` is told by the compiler when a word is added.
|
|
660
|
+
* - `granted` — keep serving; `ttlMs` is how long the lease lasts, a DURATION, never a wall-clock instant: the engine counts
|
|
661
|
+
* it from the instant it SENT the request, on its own monotonic clock (`expiresAt = sendAt + ttlMs`), and the center keeps
|
|
662
|
+
* its own expiry from its store time — the two sides never compare clocks, so no clock agreement is needed.
|
|
663
|
+
* - `denied` — this instance must stop: `reason` is {@link ServingLeaseDeniedReason}. The ONLY arm that obliges an engine to
|
|
664
|
+
* stop serving, and it is issued on POSITIVE evidence only: the center keeps, per (scope, worker), a persisted generation
|
|
665
|
+
* floor that is never garbage-collected, and a request whose generation lies below it is `superseded`; `retired` likewise
|
|
666
|
+
* rests on a record the center holds. A center that merely holds no such record does not deny — it answers `unknown`.
|
|
667
|
+
* - `unknown` — the center holds no lifecycle object for this instance right now (restoring its store, migrating scopes, not
|
|
668
|
+
* yet claimed). It says NOTHING about whether the instance should serve, so it is not a kind of `denied` and absence of the
|
|
669
|
+
* object NEVER turns into a denial: it carries no reason, and an engine never drains on it. To the engine it is the same
|
|
670
|
+
* case as an unreachable center.
|
|
671
|
+
* This is the law for what a center SENDS. A reader does not apply it to `reason`: {@link readServingLeaseResponse} judges by
|
|
672
|
+
* the `verdict` key and keeps a `denied` a `denied` even when its reason is outside the set ({@link ServingLeaseReading}). A
|
|
673
|
+
* reply whose `verdict` itself is outside the three words (or a `granted` with no valid `ttlMs`) is not a verdict at all and
|
|
674
|
+
* reads as `undefined` — an unreachable center, never `denied`, never `granted`.
|
|
675
|
+
*/
|
|
676
|
+
export declare const ServingLeaseResponse: z.ZodDiscriminatedUnion<"verdict", [z.ZodObject<{
|
|
677
|
+
verdict: z.ZodLiteral<"granted">;
|
|
678
|
+
ttlMs: z.ZodNumber;
|
|
679
|
+
}, "strip", z.ZodTypeAny, {
|
|
680
|
+
verdict: "granted";
|
|
681
|
+
ttlMs: number;
|
|
682
|
+
}, {
|
|
683
|
+
verdict: "granted";
|
|
684
|
+
ttlMs: number;
|
|
685
|
+
}>, z.ZodObject<{
|
|
686
|
+
verdict: z.ZodLiteral<"denied">;
|
|
687
|
+
reason: z.ZodEnum<["superseded", "retired"]>;
|
|
688
|
+
}, "strip", z.ZodTypeAny, {
|
|
689
|
+
reason: "superseded" | "retired";
|
|
690
|
+
verdict: "denied";
|
|
691
|
+
}, {
|
|
692
|
+
reason: "superseded" | "retired";
|
|
693
|
+
verdict: "denied";
|
|
694
|
+
}>, z.ZodObject<{
|
|
695
|
+
verdict: z.ZodLiteral<"unknown">;
|
|
696
|
+
}, "strip", z.ZodTypeAny, {
|
|
697
|
+
verdict: "unknown";
|
|
698
|
+
}, {
|
|
699
|
+
verdict: "unknown";
|
|
700
|
+
}>]>;
|
|
701
|
+
export type ServingLeaseResponse = z.infer<typeof ServingLeaseResponse>;
|
|
702
|
+
/** The reason word a READER puts in place of a `denied` reason it cannot place (outside {@link SERVING_LEASE_DENIED_REASONS},
|
|
703
|
+
* missing, not a string). Read-face only: a producer never sends it, and the strict {@link ServingLeaseResponse} refuses it. */
|
|
704
|
+
export declare const SERVING_LEASE_UNKNOWN_REASON = "unknown_reason";
|
|
705
|
+
/** The read face of {@link ServingLeaseResponse} — same three arms, with the `denied` arm tolerant about `reason`. */
|
|
706
|
+
declare const ServingLeaseReadFace: z.ZodDiscriminatedUnion<"verdict", [z.ZodObject<{
|
|
707
|
+
verdict: z.ZodLiteral<"granted">;
|
|
708
|
+
ttlMs: z.ZodNumber;
|
|
709
|
+
}, "strip", z.ZodTypeAny, {
|
|
710
|
+
verdict: "granted";
|
|
711
|
+
ttlMs: number;
|
|
712
|
+
}, {
|
|
713
|
+
verdict: "granted";
|
|
714
|
+
ttlMs: number;
|
|
715
|
+
}>, z.ZodObject<{
|
|
716
|
+
verdict: z.ZodLiteral<"denied">;
|
|
717
|
+
reason: z.ZodEffects<z.ZodUnknown, "superseded" | "retired" | "unknown_reason", unknown>;
|
|
718
|
+
}, "strip", z.ZodTypeAny, {
|
|
719
|
+
reason: "superseded" | "retired" | "unknown_reason";
|
|
720
|
+
verdict: "denied";
|
|
721
|
+
}, {
|
|
722
|
+
verdict: "denied";
|
|
723
|
+
reason?: unknown;
|
|
724
|
+
}>, z.ZodObject<{
|
|
725
|
+
verdict: z.ZodLiteral<"unknown">;
|
|
726
|
+
}, "strip", z.ZodTypeAny, {
|
|
727
|
+
verdict: "unknown";
|
|
728
|
+
}, {
|
|
729
|
+
verdict: "unknown";
|
|
730
|
+
}>]>;
|
|
731
|
+
/** The verdict words as a runtime list. `satisfies` bites a word the union does not have; the converse (a union arm missing
|
|
732
|
+
* from this list) is pinned by a test that reads the union's discriminators. */
|
|
733
|
+
export declare const SERVING_LEASE_VERDICTS: readonly ["granted", "denied", "unknown"];
|
|
734
|
+
/** Center side: the request body of a serving-lease renewal. Shape mismatch / non-object / throwing accessor ⇒ `undefined`,
|
|
735
|
+
* never throws (same guarded reader as every other read in this file). */
|
|
736
|
+
export declare function readServingLeaseRequest(body: unknown): ServingLeaseRequest | undefined;
|
|
737
|
+
/**
|
|
738
|
+
* What {@link readServingLeaseResponse} returns: {@link ServingLeaseResponse} plus the one thing a reader adds — a `denied`
|
|
739
|
+
* whose reason it could not place carries {@link SERVING_LEASE_UNKNOWN_REASON}. Two faces on purpose (the failure direction
|
|
740
|
+
* must match the meaning): the strict wire law says what a center may SEND (a closed two-word reason set); the reader judges
|
|
741
|
+
* by the `verdict` key alone, so a center that learns a third reason word cannot make an older engine read `denied` as
|
|
742
|
+
* `undefined` and keep serving. Branch on `verdict`; `reason` is for the log line.
|
|
743
|
+
*/
|
|
744
|
+
export type ServingLeaseReading = z.output<typeof ServingLeaseReadFace>;
|
|
745
|
+
/** Engine side: the reply to a renewal, judged by its `verdict` key: `granted` needs a valid `ttlMs`, `denied` is `denied`
|
|
746
|
+
* whatever its `reason` (see {@link ServingLeaseReading}), `unknown` is `unknown`. A non-object, a throwing accessor, a
|
|
747
|
+
* `verdict` outside the three words or a `granted` without a valid `ttlMs` ⇒ `undefined`, never throws — and `undefined`
|
|
748
|
+
* is read like an unreachable center, never like `denied` and never like `granted`. */
|
|
749
|
+
export declare function readServingLeaseResponse(body: unknown): ServingLeaseReading | undefined;
|
|
750
|
+
/**
|
|
751
|
+
* What the CENTER says it can do: capability bits an engine reads before it starts using a center-side endpoint.
|
|
752
|
+
* - `servingLease` — the center answers serving-lease renewals ({@link ServingLeaseRequest} / {@link ServingLeaseResponse}).
|
|
753
|
+
*
|
|
754
|
+
* A bit is `true` or absent — one spelling for "not supported" (there is no `false`). Absent (an older center, a body without
|
|
755
|
+
* the key, a body that cannot be read) means the center does not support the lease, and the engine then neither renews a
|
|
756
|
+
* lease nor is fenced by one: this is the compatibility gate that lets an executor ship ahead of the center without pushing
|
|
757
|
+
* running engines into lease expiry. It is a gate, not a verdict — nothing in this shape says "the center refuses leases".
|
|
758
|
+
* A reader asks `readCenterCapabilities(body)?.servingLease === true` and starts the lease client only on that.
|
|
759
|
+
* Unknown bits are stripped (a newer center's extra bits never break an older reader); a known bit whose value is not `true`
|
|
760
|
+
* makes the whole read `undefined`, which reads exactly as absent.
|
|
761
|
+
* This package fixes the shape and the reader, not which center endpoint carries the object.
|
|
762
|
+
*/
|
|
763
|
+
export declare const CenterCapabilities: z.ZodObject<{
|
|
764
|
+
servingLease: z.ZodOptional<z.ZodLiteral<true>>;
|
|
765
|
+
}, "strip", z.ZodTypeAny, {
|
|
766
|
+
servingLease?: true | undefined;
|
|
767
|
+
}, {
|
|
768
|
+
servingLease?: true | undefined;
|
|
769
|
+
}>;
|
|
770
|
+
export type CenterCapabilities = z.infer<typeof CenterCapabilities>;
|
|
771
|
+
/** Engine side: the center's capability object. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws
|
|
772
|
+
* (and `undefined` means "no capability said" — the same as every bit absent). */
|
|
773
|
+
export declare function readCenterCapabilities(body: unknown): CenterCapabilities | undefined;
|
|
774
|
+
/**
|
|
775
|
+
* The drain endpoint's request: "put this instance into draining, by this deadline". One persisted action, one validation,
|
|
776
|
+
* whether the center calls directly or an executor retries it. Two lanes, one shape: on the Docker lane (and for a direct
|
|
777
|
+
* call from the center) this HTTP request is how an instance is drained; on the Kubernetes lane no per-pod HTTP call is made
|
|
778
|
+
* (per-pod addressing and RBAC make it the wrong tool) — an instance is drained by its serving lease being denied
|
|
779
|
+
* ({@link ServingLeaseResponse}), after which the engine puts itself into draining.
|
|
780
|
+
* - `actionId` — idempotency key: the same id again echoes the first answer ({@link DrainReceipt}`.state` =
|
|
781
|
+
* `already_draining`) and changes nothing.
|
|
782
|
+
* - the identity — `scope?` / `worker` / `expectedGeneration?` / `instanceId?` — names WHICH instance the action is for. The
|
|
783
|
+
* engine compares it field by field against its own, absent against absent: an engine that is not a fleet member has no
|
|
784
|
+
* generation, so a request with no `expectedGeneration` is the standalone form, not a wildcard; `instanceId`, when given,
|
|
785
|
+
* is the engine's own per-process identity ({@link EngineSignal}`.instanceId`). A mismatch is the endpoint's refusal, not
|
|
786
|
+
* this shape's: this schema only says what a well-formed request looks like.
|
|
787
|
+
* - `deadlineAt` — epoch ms by which the engine stops on its own.
|
|
788
|
+
* - `term` — the claim term the sending executor holds for this action (center-issued, opaque); absent where no executor
|
|
789
|
+
* sits in between (a direct call, a hand-issued action).
|
|
790
|
+
*/
|
|
791
|
+
export declare const DrainRequest: z.ZodObject<{
|
|
792
|
+
actionId: z.ZodString;
|
|
793
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
794
|
+
worker: z.ZodString;
|
|
795
|
+
expectedGeneration: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
796
|
+
instanceId: z.ZodOptional<z.ZodString>;
|
|
797
|
+
deadlineAt: z.ZodNumber;
|
|
798
|
+
term: z.ZodOptional<z.ZodString>;
|
|
799
|
+
}, "strip", z.ZodTypeAny, {
|
|
800
|
+
worker: string;
|
|
801
|
+
actionId: string;
|
|
802
|
+
deadlineAt: number;
|
|
803
|
+
instanceId?: string | undefined;
|
|
804
|
+
scope?: string | undefined;
|
|
805
|
+
expectedGeneration?: string | undefined;
|
|
806
|
+
term?: string | undefined;
|
|
807
|
+
}, {
|
|
808
|
+
worker: string;
|
|
809
|
+
actionId: string;
|
|
810
|
+
deadlineAt: number;
|
|
811
|
+
instanceId?: string | undefined;
|
|
812
|
+
scope?: string | undefined;
|
|
813
|
+
expectedGeneration?: string | undefined;
|
|
814
|
+
term?: string | undefined;
|
|
815
|
+
}>;
|
|
816
|
+
export type DrainRequest = z.infer<typeof DrainRequest>;
|
|
817
|
+
/** What a drain request leaves the engine in: `draining` — this call put it into draining; `already_draining` — it already was
|
|
818
|
+
* (the same action repeated), and the receipt reports the first call's facts. */
|
|
819
|
+
export declare const DRAIN_RECEIPT_STATES: readonly ["draining", "already_draining"];
|
|
820
|
+
export declare const DrainReceiptState: z.ZodEnum<["draining", "already_draining"]>;
|
|
821
|
+
export type DrainReceiptState = z.infer<typeof DrainReceiptState>;
|
|
822
|
+
/** The drain endpoint's answer: the engine's draining fact ({@link EngineDraining} — the same object {@link EngineSignal}
|
|
823
|
+
* carries as `draining`, extended, not copied) plus how this call found the engine. */
|
|
824
|
+
export declare const DrainReceipt: z.ZodObject<{
|
|
825
|
+
actionId: z.ZodString;
|
|
826
|
+
since: z.ZodNumber;
|
|
827
|
+
deadlineAt: z.ZodNumber;
|
|
828
|
+
} & {
|
|
829
|
+
state: z.ZodEnum<["draining", "already_draining"]>;
|
|
830
|
+
}, "strip", z.ZodTypeAny, {
|
|
831
|
+
since: number;
|
|
832
|
+
actionId: string;
|
|
833
|
+
deadlineAt: number;
|
|
834
|
+
state: "draining" | "already_draining";
|
|
835
|
+
}, {
|
|
836
|
+
since: number;
|
|
837
|
+
actionId: string;
|
|
838
|
+
deadlineAt: number;
|
|
839
|
+
state: "draining" | "already_draining";
|
|
840
|
+
}>;
|
|
841
|
+
export type DrainReceipt = z.infer<typeof DrainReceipt>;
|
|
842
|
+
/** Engine side: the request body of the drain endpoint. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
843
|
+
export declare function readDrainRequest(body: unknown): DrainRequest | undefined;
|
|
844
|
+
/** Caller side: the drain endpoint's answer. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
845
|
+
export declare function readDrainReceipt(body: unknown): DrainReceipt | undefined;
|
|
433
846
|
export declare const DISPATCH_STRATEGIES: readonly ["round-robin", "least-inflight", "consistent-hash"];
|
|
434
847
|
export type DispatchStrategy = (typeof DISPATCH_STRATEGIES)[number];
|
|
435
848
|
/** Per-worker dispatch policy (center-configured; the gateway is OPTIONAL — D3: TOC/self-maintained TOB go
|
|
@@ -749,3 +1162,4 @@ export interface AggregatedPrincipalUsage {
|
|
|
749
1162
|
* view). Pure — storage/dedup of replayed batches is the caller's concern (key batches by
|
|
750
1163
|
* worker+instanceId+windowStartMs). */
|
|
751
1164
|
export declare function aggregateUsage(reports: UsageReport[]): Record<string, AggregatedPrincipalUsage>;
|
|
1165
|
+
export {};
|
package/dist/fleet.js
CHANGED
|
@@ -40,13 +40,16 @@ export function drainVerdict(s) {
|
|
|
40
40
|
const grace = Math.max(DRAIN_GRACE_FLOOR_MS, s.graceMs);
|
|
41
41
|
return s.nowMs - s.startedMs >= grace ? "exit_forced" : "wait";
|
|
42
42
|
}
|
|
43
|
+
/** A wall-clock instant as epoch milliseconds — one quantity, one rule, wherever the fleet contract carries a point in time. */
|
|
44
|
+
const EpochMs = z.number().int().nonnegative();
|
|
43
45
|
/**
|
|
44
46
|
* "This instance must restart to apply config" — the engine's OWN observation (S-741), defined ONCE for both exits that
|
|
45
47
|
* carry it: the fleet heartbeat ({@link InstanceAnnouncement}`.restart`, typed) and GET /health (`restart`, read through
|
|
46
48
|
* {@link readRestartSignal} — {@link InstanceHealth} keeps carrying it untyped, see there). One mint point, two exits —
|
|
47
49
|
* never a second judgement.
|
|
48
50
|
*
|
|
49
|
-
* Shape = what the server already ships on /health (`restart: { reasons, version, since }
|
|
51
|
+
* Shape = what the server already ships on /health (`restart: { reasons, version, since }`, plus the optional `withheld`
|
|
52
|
+
* qualifier below). PRESENCE is the signal:
|
|
50
53
|
* present ⇔ a restart is required, absent ⇔ not; there is deliberately no `required` boolean inside (a boolean next
|
|
51
54
|
* to presence is redundant, and two spellings of one fact drift).
|
|
52
55
|
* - `reasons` — which restart-to-apply slices differ from the value baked at boot (the vocabulary is the engine's;
|
|
@@ -55,35 +58,45 @@ export function drainVerdict(s) {
|
|
|
55
58
|
* later publishes, so a "one restart per version" dedupe bounds restarts per config version, not per reason set).
|
|
56
59
|
* Judged exactly as `readEffectiveWire` judges a config version (a finite number) — one quantity, one rule.
|
|
57
60
|
* - `since` — epoch ms when this restart first became required (stable across refreshes while `reasons` holds).
|
|
61
|
+
* - `withheld` — optional: the ENGINE's own verdict that this beat must not be handed to a new generation ("a restart is
|
|
62
|
+
* required, but replacing this instance right now would lose state it holds"). Absent ≡ `false` — an explicit `false`
|
|
63
|
+
* on the wire reads the same as the key being absent, and a reader never mints the key when the producer did not send
|
|
64
|
+
* it. It is a qualifier INSIDE the restart signal, never a signal of its own: it exists only where `restart` does.
|
|
58
65
|
*/
|
|
59
66
|
export const RestartSignal = z.object({
|
|
60
67
|
reasons: z.array(z.string().min(1)).min(1),
|
|
61
68
|
version: z.number().finite(),
|
|
62
|
-
since:
|
|
69
|
+
since: EpochMs,
|
|
70
|
+
withheld: z.boolean().optional(),
|
|
63
71
|
});
|
|
64
72
|
/**
|
|
65
|
-
* The ONE
|
|
66
|
-
*
|
|
67
|
-
*
|
|
73
|
+
* The ONE guarded body reader behind every wire-shape read in this file: the engine-owned signals on /health
|
|
74
|
+
* ({@link readRestartSignal}, {@link readModelRefusals}, {@link readEngineSignal}) and the request / reply bodies of the
|
|
75
|
+
* fleet endpoints ({@link readServingLeaseRequest}, {@link readServingLeaseResponse}, {@link readDrainRequest},
|
|
76
|
+
* {@link readDrainReceipt}). `pick(body)` — one key of the body, or the body itself — is judged against that shape's one
|
|
77
|
+
* schema. Shape mismatch, a non-object body, even a throwing accessor ⇒ `undefined` — never throws (a probe must not die
|
|
78
|
+
* on one odd body). A second hand-written guard anywhere else in this package is the defect, not a convenience.
|
|
68
79
|
*/
|
|
69
|
-
function
|
|
80
|
+
function readSignalBody(body, pick, schema) {
|
|
70
81
|
try {
|
|
71
|
-
if (
|
|
82
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
72
83
|
return undefined;
|
|
73
|
-
const parsed = schema.safeParse(
|
|
84
|
+
const parsed = schema.safeParse(pick(body));
|
|
74
85
|
return parsed.success ? parsed.data : undefined;
|
|
75
86
|
}
|
|
76
87
|
catch {
|
|
77
88
|
return undefined;
|
|
78
89
|
}
|
|
79
90
|
}
|
|
91
|
+
/** `pick` for a shape whose keys sit flat on the body itself (as opposed to under one key of it). */
|
|
92
|
+
const wholeBody = (body) => body;
|
|
80
93
|
/**
|
|
81
94
|
* The /health exit of {@link RestartSignal}: `health.restart` judged against the one shape. Shape mismatch, a missing
|
|
82
95
|
* key, a non-object body, even a throwing accessor ⇒ `undefined` — never throws (a probe must not die on one odd body).
|
|
83
96
|
* Presence law: only `restart` counts; the top-level `restartRequired` boolean the server also ships is not read.
|
|
84
97
|
*/
|
|
85
98
|
export function readRestartSignal(health) {
|
|
86
|
-
return
|
|
99
|
+
return readSignalBody(health, (h) => h.restart, RestartSignal);
|
|
87
100
|
}
|
|
88
101
|
/**
|
|
89
102
|
* The model-refusal cause codes (S-768) — CLOSED sets, owned HERE, never a hand-copied second list. Adding a code is
|
|
@@ -177,7 +190,7 @@ const isModelPlaneRefusalCode = (c) => MODEL_PLANE_REFUSAL_CODES.includes(c);
|
|
|
177
190
|
* at all — it answers the key only and never judges the engine's version (see {@link InstanceAnnouncement}`.modelRefusals`).
|
|
178
191
|
*/
|
|
179
192
|
export function readModelRefusals(body) {
|
|
180
|
-
const wire =
|
|
193
|
+
const wire = readSignalBody(body, (b) => b.modelRefusals, ModelRefusalsWire);
|
|
181
194
|
if (wire === undefined)
|
|
182
195
|
return undefined;
|
|
183
196
|
const entries = Object.fromEntries(Object.entries(wire.entries).filter((e) => isModelEntryRefusalCode(e[1])));
|
|
@@ -205,7 +218,11 @@ export const InstanceHealth = z
|
|
|
205
218
|
// S-768: `modelRefusals` rides the passthrough untyped for the same reason — read it with {@link readModelRefusals}.
|
|
206
219
|
})
|
|
207
220
|
.passthrough();
|
|
208
|
-
/** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch).
|
|
221
|
+
/** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch).
|
|
222
|
+
* Its input is the boolean-`draining` dialect of {@link InstanceHealth}. An {@link EngineSignal} types `draining` as an
|
|
223
|
+
* object, so it does not typecheck here (on purpose: `!== true` would read an object-valued `draining` as "ready").
|
|
224
|
+
* The next major retires the boolean pair (`draining` / `drainingSince`) and gives both this predicate and
|
|
225
|
+
* {@link isRoutable} `EngineSignal` as their input. */
|
|
209
226
|
export function isReadyForTraffic(h) {
|
|
210
227
|
return h.draining !== true;
|
|
211
228
|
}
|
|
@@ -266,13 +283,271 @@ export function isInstanceStale(r, nowMs) {
|
|
|
266
283
|
return nowMs - r.lastSeenMs > STALE_HEARTBEAT_MULTIPLIER * r.heartbeatMs;
|
|
267
284
|
}
|
|
268
285
|
/** Health fusion: routable ⇔ self-declared ready ∧ not draining ∧ not stale ∧ observer does not veto.
|
|
269
|
-
* Two sources merge as an AND — EITHER source's "not ready" removes the instance (fail-closed).
|
|
286
|
+
* Two sources merge as an AND — EITHER source's "not ready" removes the instance (fail-closed).
|
|
287
|
+
* Its input is the stored heartbeat row (boolean `draining`); it moves to {@link EngineSignal} in the same major as
|
|
288
|
+
* {@link isReadyForTraffic}. */
|
|
270
289
|
export function isRoutable(r, nowMs) {
|
|
271
290
|
return r.ready && !r.draining && r.observedReady !== false && !isInstanceStale(r, nowMs);
|
|
272
291
|
}
|
|
273
292
|
export function routableInstances(rs, nowMs) {
|
|
274
293
|
return rs.filter((r) => isRoutable(r, nowMs));
|
|
275
294
|
}
|
|
295
|
+
// ── Domain 2b: the engine's own signal, the serving lease, the drain endpoint ─────────────────────────────
|
|
296
|
+
/**
|
|
297
|
+
* The engine's draining fact — ONE object wherever the engine says "I am draining": inside {@link EngineSignal}`.draining`
|
|
298
|
+
* and as the body of a {@link DrainReceipt}.
|
|
299
|
+
* - `actionId` — the action that put the engine into draining (idempotency key of the drain request).
|
|
300
|
+
* - `since` — epoch ms when draining began; it does not move when the same action repeats.
|
|
301
|
+
* - `deadlineAt` — epoch ms by which the engine stops on its own.
|
|
302
|
+
*/
|
|
303
|
+
export const EngineDraining = z.object({
|
|
304
|
+
actionId: z.string().min(1),
|
|
305
|
+
since: EpochMs,
|
|
306
|
+
deadlineAt: EpochMs,
|
|
307
|
+
});
|
|
308
|
+
/**
|
|
309
|
+
* A generation is a non-negative integer written as its CANONICAL decimal string: `"0"`, `"7"`, `"10"` — no sign, no
|
|
310
|
+
* leading zero (`"07"` is not a generation), no fraction, no whitespace, ASCII digits only, and a string, never a number.
|
|
311
|
+
* One spelling per value, so equality is string equality and the order is numeric ({@link compareGeneration}) — the center's
|
|
312
|
+
* persisted generation floor and every "is this generation below the floor" judgement rest on that order. The renderer writes
|
|
313
|
+
* the value into {@link FLEET_GENERATION_ENV}; an env value that is present but not canonical is a misconfiguration to refuse
|
|
314
|
+
* loudly, not an absent generation (absence is what marks a non-member). Wherever a wire shape here carries a generation
|
|
315
|
+
* ({@link EngineSignal}`.generation`, {@link ServingLeaseRequest}, {@link DrainRequest}`.expectedGeneration`) it is judged by
|
|
316
|
+
* this one predicate.
|
|
317
|
+
*/
|
|
318
|
+
const CANONICAL_GENERATION = /^(?:0|[1-9][0-9]*)$/;
|
|
319
|
+
export function isCanonicalGeneration(value) {
|
|
320
|
+
return typeof value === "string" && CANONICAL_GENERATION.test(value);
|
|
321
|
+
}
|
|
322
|
+
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
323
|
+
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
324
|
+
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
325
|
+
* first — the wire shapes of this file already do. */
|
|
326
|
+
export function compareGeneration(a, b) {
|
|
327
|
+
if (!isCanonicalGeneration(a) || !isCanonicalGeneration(b)) {
|
|
328
|
+
throw new TypeError(`compareGeneration: a generation is a canonical non-negative decimal string (got ${JSON.stringify(isCanonicalGeneration(a) ? b : a)})`);
|
|
329
|
+
}
|
|
330
|
+
if (a.length !== b.length)
|
|
331
|
+
return a.length < b.length ? -1 : 1; // canonical ⇒ no leading zeros ⇒ more digits is larger
|
|
332
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
333
|
+
}
|
|
334
|
+
/** The generation field of every wire shape that carries one — a string judged by {@link isCanonicalGeneration}. */
|
|
335
|
+
const Generation = z.string().refine(isCanonicalGeneration, { message: "a generation is a canonical non-negative decimal string (no sign, no leading zero)" });
|
|
336
|
+
/**
|
|
337
|
+
* Everything the ENGINE itself mints about its own state, as one object — the closed form of the engine-owned subset of the
|
|
338
|
+
* /health self-description ({@link InstanceHealth} is that self-description: an open record, `.passthrough()`, which also
|
|
339
|
+
* carries what the host adds — build, pid, port, store posture). One mint point; the heartbeat and `/health` carry the same
|
|
340
|
+
* keys with the same values, flat (`ready`, `draining`, `restart`, … sit directly on the body, no wrapper key), and
|
|
341
|
+
* {@link readEngineSignal} is the one reader of either.
|
|
342
|
+
*
|
|
343
|
+
* - `ready` — always said (never "absent while ready"): the engine can take new work right now.
|
|
344
|
+
* - `draining` — present ⇔ draining ({@link EngineDraining}). NOTE the dialect: {@link InstanceHealth}`.draining` and
|
|
345
|
+
* {@link InstanceAnnouncement}`.draining` are BOOLEANS (the 8.0 dialect, kept untouched); here it is an object. A body
|
|
346
|
+
* whose `draining` is a boolean is therefore not an `EngineSignal` and reads as `undefined`.
|
|
347
|
+
* - `restart` — the one {@link RestartSignal}, same schema object, no copy.
|
|
348
|
+
* - `generation` — echo of the env the config renderer set on this instance ({@link FLEET_GENERATION_ENV}), a canonical
|
|
349
|
+
* decimal string ({@link isCanonicalGeneration}). Absent ⇔ not a fleet member (a single-machine engine): it takes no lease
|
|
350
|
+
* and is not fenced.
|
|
351
|
+
* - `instanceId` — the engine's OWN process identity, minted by the engine every time the process starts (never read from
|
|
352
|
+
* env: replicas of one Deployment share one env, and a restarted container reuses its env, so env cannot tell two
|
|
353
|
+
* processes apart). It is present on a standalone engine too — only `generation` marks fleet membership.
|
|
354
|
+
* - `leaseUntil` — epoch ms until which the engine holds its serving lease, as the engine computed it from the last
|
|
355
|
+
* `ttlMs` it was granted ({@link ServingLeaseResponse}); absent when it holds none.
|
|
356
|
+
* - `configTargetVersion` / `configAppliedVersion` — the config-center version the engine was told to converge on, and the
|
|
357
|
+
* one it has applied (opaque strings; equal ⇔ converged).
|
|
358
|
+
*
|
|
359
|
+
* Reading: `EngineSignal` is parsed with the default strip — a whole /health body goes in, only these keys come out. A
|
|
360
|
+
* shape violation in ANY key makes the whole signal `undefined` (it is never "the rest of it"). `undefined` means "no
|
|
361
|
+
* signal this reader can say" — an engine that predates the keys, or a malformed body — and is NEVER "ready": a caller on
|
|
362
|
+
* the routing axis treats it as not routable unless it deliberately falls back to the boolean dialect of
|
|
363
|
+
* {@link InstanceHealth}.
|
|
364
|
+
*/
|
|
365
|
+
export const EngineSignal = z.object({
|
|
366
|
+
ready: z.boolean(),
|
|
367
|
+
draining: EngineDraining.optional(),
|
|
368
|
+
restart: RestartSignal.optional(),
|
|
369
|
+
generation: Generation.optional(),
|
|
370
|
+
instanceId: z.string().min(1).optional(),
|
|
371
|
+
leaseUntil: EpochMs.optional(),
|
|
372
|
+
configTargetVersion: z.string().min(1).optional(),
|
|
373
|
+
configAppliedVersion: z.string().min(1).optional(),
|
|
374
|
+
});
|
|
375
|
+
/**
|
|
376
|
+
* The reader of {@link EngineSignal}, for either exit (a /health body or a heartbeat body — the keys are flat on both): the
|
|
377
|
+
* same guarded body reader as {@link readRestartSignal} and {@link readModelRefusals}, so the same odd input gets the same
|
|
378
|
+
* answer from all three. Shape mismatch, a missing `ready`, a non-object body, even a throwing accessor ⇒ `undefined` —
|
|
379
|
+
* never throws. See {@link EngineSignal} for what `undefined` means (not "ready").
|
|
380
|
+
*/
|
|
381
|
+
export function readEngineSignal(health) {
|
|
382
|
+
return readSignalBody(health, wholeBody, EngineSignal);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The env name a fleet member's generation travels under — the config renderer sets it, the engine reads it back and echoes
|
|
386
|
+
* it as {@link EngineSignal}`.generation`. Both sides import THIS constant; neither spells the name. The value is the
|
|
387
|
+
* generation this instance belongs to (opaque string); absent from the engine's env ⇔ not a fleet member (the standalone
|
|
388
|
+
* form). There is deliberately NO env name for the instance id: an instance's identity is minted by the engine per process
|
|
389
|
+
* ({@link EngineSignal}`.instanceId`), and env cannot carry it (see there).
|
|
390
|
+
*/
|
|
391
|
+
export const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
392
|
+
/**
|
|
393
|
+
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
394
|
+
* engine renews it with the center; the center answers per the lifecycle object it holds for that instance. Three constants
|
|
395
|
+
* fix the clock, all in ms:
|
|
396
|
+
* - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
|
|
397
|
+
* - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
|
|
398
|
+
* the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
|
|
399
|
+
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the center's clock-skew allowance: it adds this to a lease's recorded expiry
|
|
400
|
+
* before it counts that lease as over. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a
|
|
401
|
+
* second constant that could drift from it.
|
|
402
|
+
*/
|
|
403
|
+
export const SERVING_LEASE_TTL_DEFAULT_MS = 600_000;
|
|
404
|
+
export const SERVING_LEASE_TTL_FLOOR_MS = 60_000;
|
|
405
|
+
export const SERVING_LEASE_DRIFT_MARGIN_MS = DEFAULT_HEARTBEAT_MS;
|
|
406
|
+
/**
|
|
407
|
+
* The ONE place a configured lease TTL becomes a usable one: not a number (absent, `NaN`, infinite, another type) ⇒
|
|
408
|
+
* {@link SERVING_LEASE_TTL_DEFAULT_MS}; a number below {@link SERVING_LEASE_TTL_FLOOR_MS} (zero and negatives included) ⇒ the
|
|
409
|
+
* floor; otherwise the number, truncated to whole ms, with no ceiling. It is for the ISSUING side's configuration. A reply's
|
|
410
|
+
* `ttlMs` is used exactly as granted and is never clamped upward — an engine that stretched a short grant to the floor would
|
|
411
|
+
* hold the lease past what the center recorded, which is the one thing the fence exists to prevent.
|
|
412
|
+
*/
|
|
413
|
+
export function clampServingLeaseTtlMs(configuredMs) {
|
|
414
|
+
if (typeof configuredMs !== "number" || !Number.isFinite(configuredMs))
|
|
415
|
+
return SERVING_LEASE_TTL_DEFAULT_MS;
|
|
416
|
+
return Math.max(SERVING_LEASE_TTL_FLOOR_MS, Math.floor(configuredMs));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Engine → center: "may this instance keep serving?" The identity triple names the instance exactly — a lease is never
|
|
420
|
+
* asked for by worker name alone. `generation` is the env echo ({@link FLEET_GENERATION_ENV}); `instanceId` is the engine's
|
|
421
|
+
* own per-process identity ({@link EngineSignal}`.instanceId`), so a restarted process is a NEW instance to the center.
|
|
422
|
+
* `scope` is the tenant scope the worker lives in (absent = the single-scope deployment). Only fleet members ask: an engine
|
|
423
|
+
* without a generation (the standalone form) has no lease and sends nothing.
|
|
424
|
+
*/
|
|
425
|
+
export const ServingLeaseRequest = z.object({
|
|
426
|
+
worker: z.string().min(1),
|
|
427
|
+
scope: z.string().min(1).optional(),
|
|
428
|
+
generation: Generation,
|
|
429
|
+
instanceId: z.string().min(1),
|
|
430
|
+
});
|
|
431
|
+
/**
|
|
432
|
+
* Why a lease is refused — a CLOSED set of exactly two words, owned here:
|
|
433
|
+
* - `superseded` — a newer generation of this worker has taken over; this instance is the old one.
|
|
434
|
+
* - `retired` — the center has retired this instance (or the worker it belongs to).
|
|
435
|
+
* Only {@link ServingLeaseResponse}'s `denied` arm carries a reason; `unknown` has none, by shape.
|
|
436
|
+
*/
|
|
437
|
+
export const SERVING_LEASE_DENIED_REASONS = ["superseded", "retired"];
|
|
438
|
+
export const ServingLeaseDeniedReason = z.enum(SERVING_LEASE_DENIED_REASONS);
|
|
439
|
+
const ServingLeaseGranted = z.object({ verdict: z.literal("granted"), ttlMs: z.number().int().positive() });
|
|
440
|
+
const ServingLeaseDenied = z.object({ verdict: z.literal("denied"), reason: ServingLeaseDeniedReason });
|
|
441
|
+
const ServingLeaseUnknown = z.object({ verdict: z.literal("unknown") });
|
|
442
|
+
/**
|
|
443
|
+
* Center → engine: the verdict, a CLOSED set of three words ({@link SERVING_LEASE_VERDICTS}) — the arms of one
|
|
444
|
+
* discriminated union, so a reader that switches on `verdict` is told by the compiler when a word is added.
|
|
445
|
+
* - `granted` — keep serving; `ttlMs` is how long the lease lasts, a DURATION, never a wall-clock instant: the engine counts
|
|
446
|
+
* it from the instant it SENT the request, on its own monotonic clock (`expiresAt = sendAt + ttlMs`), and the center keeps
|
|
447
|
+
* its own expiry from its store time — the two sides never compare clocks, so no clock agreement is needed.
|
|
448
|
+
* - `denied` — this instance must stop: `reason` is {@link ServingLeaseDeniedReason}. The ONLY arm that obliges an engine to
|
|
449
|
+
* stop serving, and it is issued on POSITIVE evidence only: the center keeps, per (scope, worker), a persisted generation
|
|
450
|
+
* floor that is never garbage-collected, and a request whose generation lies below it is `superseded`; `retired` likewise
|
|
451
|
+
* rests on a record the center holds. A center that merely holds no such record does not deny — it answers `unknown`.
|
|
452
|
+
* - `unknown` — the center holds no lifecycle object for this instance right now (restoring its store, migrating scopes, not
|
|
453
|
+
* yet claimed). It says NOTHING about whether the instance should serve, so it is not a kind of `denied` and absence of the
|
|
454
|
+
* object NEVER turns into a denial: it carries no reason, and an engine never drains on it. To the engine it is the same
|
|
455
|
+
* case as an unreachable center.
|
|
456
|
+
* This is the law for what a center SENDS. A reader does not apply it to `reason`: {@link readServingLeaseResponse} judges by
|
|
457
|
+
* the `verdict` key and keeps a `denied` a `denied` even when its reason is outside the set ({@link ServingLeaseReading}). A
|
|
458
|
+
* reply whose `verdict` itself is outside the three words (or a `granted` with no valid `ttlMs`) is not a verdict at all and
|
|
459
|
+
* reads as `undefined` — an unreachable center, never `denied`, never `granted`.
|
|
460
|
+
*/
|
|
461
|
+
export const ServingLeaseResponse = z.discriminatedUnion("verdict", [ServingLeaseGranted, ServingLeaseDenied, ServingLeaseUnknown]);
|
|
462
|
+
/** The reason word a READER puts in place of a `denied` reason it cannot place (outside {@link SERVING_LEASE_DENIED_REASONS},
|
|
463
|
+
* missing, not a string). Read-face only: a producer never sends it, and the strict {@link ServingLeaseResponse} refuses it. */
|
|
464
|
+
export const SERVING_LEASE_UNKNOWN_REASON = "unknown_reason";
|
|
465
|
+
const isServingLeaseDeniedReason = (w) => SERVING_LEASE_DENIED_REASONS.includes(w);
|
|
466
|
+
/** What a reader makes of the `denied` arm: it is judged by the `verdict` key ALONE, and `reason` is diagnostic — a reason it
|
|
467
|
+
* can place is kept, anything else becomes {@link SERVING_LEASE_UNKNOWN_REASON}. The arm is still `denied`. */
|
|
468
|
+
const ServingLeaseDeniedRead = z.object({
|
|
469
|
+
verdict: z.literal("denied"),
|
|
470
|
+
reason: z.unknown().transform((r) => (isServingLeaseDeniedReason(r) ? r : SERVING_LEASE_UNKNOWN_REASON)),
|
|
471
|
+
});
|
|
472
|
+
/** The read face of {@link ServingLeaseResponse} — same three arms, with the `denied` arm tolerant about `reason`. */
|
|
473
|
+
const ServingLeaseReadFace = z.discriminatedUnion("verdict", [ServingLeaseGranted, ServingLeaseDeniedRead, ServingLeaseUnknown]);
|
|
474
|
+
/** The verdict words as a runtime list. `satisfies` bites a word the union does not have; the converse (a union arm missing
|
|
475
|
+
* from this list) is pinned by a test that reads the union's discriminators. */
|
|
476
|
+
export const SERVING_LEASE_VERDICTS = ["granted", "denied", "unknown"];
|
|
477
|
+
/** Center side: the request body of a serving-lease renewal. Shape mismatch / non-object / throwing accessor ⇒ `undefined`,
|
|
478
|
+
* never throws (same guarded reader as every other read in this file). */
|
|
479
|
+
export function readServingLeaseRequest(body) {
|
|
480
|
+
return readSignalBody(body, wholeBody, ServingLeaseRequest);
|
|
481
|
+
}
|
|
482
|
+
/** Engine side: the reply to a renewal, judged by its `verdict` key: `granted` needs a valid `ttlMs`, `denied` is `denied`
|
|
483
|
+
* whatever its `reason` (see {@link ServingLeaseReading}), `unknown` is `unknown`. A non-object, a throwing accessor, a
|
|
484
|
+
* `verdict` outside the three words or a `granted` without a valid `ttlMs` ⇒ `undefined`, never throws — and `undefined`
|
|
485
|
+
* is read like an unreachable center, never like `denied` and never like `granted`. */
|
|
486
|
+
export function readServingLeaseResponse(body) {
|
|
487
|
+
return readSignalBody(body, wholeBody, ServingLeaseReadFace);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* What the CENTER says it can do: capability bits an engine reads before it starts using a center-side endpoint.
|
|
491
|
+
* - `servingLease` — the center answers serving-lease renewals ({@link ServingLeaseRequest} / {@link ServingLeaseResponse}).
|
|
492
|
+
*
|
|
493
|
+
* A bit is `true` or absent — one spelling for "not supported" (there is no `false`). Absent (an older center, a body without
|
|
494
|
+
* the key, a body that cannot be read) means the center does not support the lease, and the engine then neither renews a
|
|
495
|
+
* lease nor is fenced by one: this is the compatibility gate that lets an executor ship ahead of the center without pushing
|
|
496
|
+
* running engines into lease expiry. It is a gate, not a verdict — nothing in this shape says "the center refuses leases".
|
|
497
|
+
* A reader asks `readCenterCapabilities(body)?.servingLease === true` and starts the lease client only on that.
|
|
498
|
+
* Unknown bits are stripped (a newer center's extra bits never break an older reader); a known bit whose value is not `true`
|
|
499
|
+
* makes the whole read `undefined`, which reads exactly as absent.
|
|
500
|
+
* This package fixes the shape and the reader, not which center endpoint carries the object.
|
|
501
|
+
*/
|
|
502
|
+
export const CenterCapabilities = z.object({
|
|
503
|
+
servingLease: z.literal(true).optional(),
|
|
504
|
+
});
|
|
505
|
+
/** Engine side: the center's capability object. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws
|
|
506
|
+
* (and `undefined` means "no capability said" — the same as every bit absent). */
|
|
507
|
+
export function readCenterCapabilities(body) {
|
|
508
|
+
return readSignalBody(body, wholeBody, CenterCapabilities);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The drain endpoint's request: "put this instance into draining, by this deadline". One persisted action, one validation,
|
|
512
|
+
* whether the center calls directly or an executor retries it. Two lanes, one shape: on the Docker lane (and for a direct
|
|
513
|
+
* call from the center) this HTTP request is how an instance is drained; on the Kubernetes lane no per-pod HTTP call is made
|
|
514
|
+
* (per-pod addressing and RBAC make it the wrong tool) — an instance is drained by its serving lease being denied
|
|
515
|
+
* ({@link ServingLeaseResponse}), after which the engine puts itself into draining.
|
|
516
|
+
* - `actionId` — idempotency key: the same id again echoes the first answer ({@link DrainReceipt}`.state` =
|
|
517
|
+
* `already_draining`) and changes nothing.
|
|
518
|
+
* - the identity — `scope?` / `worker` / `expectedGeneration?` / `instanceId?` — names WHICH instance the action is for. The
|
|
519
|
+
* engine compares it field by field against its own, absent against absent: an engine that is not a fleet member has no
|
|
520
|
+
* generation, so a request with no `expectedGeneration` is the standalone form, not a wildcard; `instanceId`, when given,
|
|
521
|
+
* is the engine's own per-process identity ({@link EngineSignal}`.instanceId`). A mismatch is the endpoint's refusal, not
|
|
522
|
+
* this shape's: this schema only says what a well-formed request looks like.
|
|
523
|
+
* - `deadlineAt` — epoch ms by which the engine stops on its own.
|
|
524
|
+
* - `term` — the claim term the sending executor holds for this action (center-issued, opaque); absent where no executor
|
|
525
|
+
* sits in between (a direct call, a hand-issued action).
|
|
526
|
+
*/
|
|
527
|
+
export const DrainRequest = z.object({
|
|
528
|
+
actionId: z.string().min(1),
|
|
529
|
+
scope: z.string().min(1).optional(),
|
|
530
|
+
worker: z.string().min(1),
|
|
531
|
+
expectedGeneration: Generation.optional(),
|
|
532
|
+
instanceId: z.string().min(1).optional(),
|
|
533
|
+
deadlineAt: EpochMs,
|
|
534
|
+
term: z.string().min(1).optional(),
|
|
535
|
+
});
|
|
536
|
+
/** What a drain request leaves the engine in: `draining` — this call put it into draining; `already_draining` — it already was
|
|
537
|
+
* (the same action repeated), and the receipt reports the first call's facts. */
|
|
538
|
+
export const DRAIN_RECEIPT_STATES = ["draining", "already_draining"];
|
|
539
|
+
export const DrainReceiptState = z.enum(DRAIN_RECEIPT_STATES);
|
|
540
|
+
/** The drain endpoint's answer: the engine's draining fact ({@link EngineDraining} — the same object {@link EngineSignal}
|
|
541
|
+
* carries as `draining`, extended, not copied) plus how this call found the engine. */
|
|
542
|
+
export const DrainReceipt = EngineDraining.extend({ state: DrainReceiptState });
|
|
543
|
+
/** Engine side: the request body of the drain endpoint. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
544
|
+
export function readDrainRequest(body) {
|
|
545
|
+
return readSignalBody(body, wholeBody, DrainRequest);
|
|
546
|
+
}
|
|
547
|
+
/** Caller side: the drain endpoint's answer. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
548
|
+
export function readDrainReceipt(body) {
|
|
549
|
+
return readSignalBody(body, wholeBody, DrainReceipt);
|
|
550
|
+
}
|
|
276
551
|
// ── Domain 3: dispatch policy (traffic + failover + LB — three domains, one gateway) ──────────────────────
|
|
277
552
|
export const DISPATCH_STRATEGIES = ["round-robin", "least-inflight", "consistent-hash"];
|
|
278
553
|
/** Per-worker dispatch policy (center-configured; the gateway is OPTIONAL — D3: TOC/self-maintained TOB go
|
package/dist/types.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
|
|
20
|
-
import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
20
|
+
import { DispatchPolicy, FLEET_GENERATION_ENV } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
21
21
|
import { carriesUserinfo, percentLayers } from "./secret-scan.js"; // package-private normalization + the userinfo judgement (not in the barrel)
|
|
22
22
|
import { EXECUTION_LANES, EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js"; // 7.0.0 M-R2: the lane / store vocabulary (one exported table)
|
|
23
23
|
import { LANE_ENV_OWNER, MIGRATION_SIGNPOST, isMigratableEnvKey, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (not in the barrel)
|
|
@@ -1569,6 +1569,8 @@ const RESERVED_ENV_FIELD = Object.freeze({
|
|
|
1569
1569
|
PG_DATABASE: "runtime.store.db.database", PG_PASSWORD: "secretRefs.dbPasswordEnv",
|
|
1570
1570
|
// ── runtime.execution(7.0.0):车道词两键 + 车道表里每个名(派生,不手抄)
|
|
1571
1571
|
REMOTE_EXEC: "runtime.execution.lane",
|
|
1572
|
+
// ── fleet 成员身份:渲染器按该 worker 的代际写它,引擎读回并回声成 EngineSignal.generation —— 没有 WorkerSpec 字段能写它
|
|
1573
|
+
[FLEET_GENERATION_ENV]: "(set by the config renderer from the worker's generation — not configurable)",
|
|
1572
1574
|
REQUIRE_EXECUTION_LANE: "runtime.execution.lane (the renderer emits REQUIRE_EXECUTION_LANE=true for every declared lane)",
|
|
1573
1575
|
...Object.fromEntries([...LANE_ENV_OWNER].map(([name, owner]) => [name, laneEnvSeat(name, owner)])),
|
|
1574
1576
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|