@sema-agent/server 7.119.0 → 7.121.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/MIGRATION.md +129 -0
- package/USAGE.md +72 -23
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -2
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
- package/dist/boot/config-center.d.ts +5 -7
- package/dist/boot/config-center.js +37 -13
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +54 -16
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-09-leader.d.ts +2 -9
- package/dist/boot/stage-10-http-server.d.ts +1 -9
- package/dist/boot/stage-10-http-server.js +31 -8
- package/dist/capabilities/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +13 -7
- package/dist/config-center/center-request.d.ts +16 -1
- package/dist/config-center/center-request.js +36 -5
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +46 -3
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +33 -4
- package/dist/config.js +107 -15
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/fleet/drain-entry.d.ts +87 -0
- package/dist/fleet/drain-entry.js +35 -0
- package/dist/fleet/engine-signal.d.ts +144 -0
- package/dist/fleet/engine-signal.js +60 -0
- package/dist/fleet/serving-lease.d.ts +47 -0
- package/dist/fleet/serving-lease.js +235 -0
- package/dist/fleet-client.d.ts +35 -19
- package/dist/fleet-client.js +49 -25
- package/dist/http/approval-sweeps.js +3 -5
- package/dist/http/dispatch.js +18 -13
- package/dist/http/health-self-report.d.ts +60 -0
- package/dist/http/health-self-report.js +53 -0
- package/dist/http/principal-gate.d.ts +8 -1
- package/dist/http/principal-gate.js +1 -0
- package/dist/http/readiness.d.ts +25 -5
- package/dist/http/readiness.js +31 -6
- package/dist/http/resume-legs.js +5 -1
- package/dist/http/route-ctx.d.ts +6 -4
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -1
- package/dist/http/routes/a2a-serve.d.ts +1 -1
- package/dist/http/routes/a2a-serve.js +5 -5
- package/dist/http/routes/admin-drain.d.ts +12 -31
- package/dist/http/routes/admin-drain.js +15 -9
- package/dist/http/routes/approvals-assistant.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +3 -1
- package/dist/http/routes/devices.js +6 -5
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/leader.js +1 -1
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/notify-wake.d.ts +1 -1
- package/dist/http/routes/notify-wake.js +1 -1
- package/dist/http/routes/rules.js +14 -6
- package/dist/http/routes/runs.d.ts +2 -2
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/side-query.d.ts +1 -1
- package/dist/http/routes/side-query.js +1 -1
- package/dist/http/routes/tasks.d.ts +2 -2
- package/dist/http/routes/tasks.js +3 -3
- package/dist/http/server.d.ts +7 -21
- package/dist/http/server.js +20 -1
- package/dist/http/wire-types.d.ts +7 -1
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/model-reach-scope.d.ts +15 -3
- package/dist/model-reach-scope.js +3 -2
- package/dist/model-unreachable-notice.d.ts +2 -1
- package/dist/model-unreachable-notice.js +1 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +20 -0
- package/dist/observability/fail-open.js +20 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -6
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/request-key-closure.js +1 -1
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +16 -3
- package/dist/rules-consent.js +8 -4
- package/dist/run-local.js +2 -0
- package/dist/runs.js +1 -1
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/tool-approval.d.ts +7 -0
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +78 -0
- package/dist/trace/self-minted-wire.js +32 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- package/package.json +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎信号的**一只铸点**(编排器长远稿片 2 件 1;契约型 = settings-schema `./fleet` 的 `EngineSignal` / `RestartSignal.withheld` / `LeaseState` / `InstanceAnnouncement`)。
|
|
3
|
+
*
|
|
4
|
+
* ## 规则(先写规则,再写码)
|
|
5
|
+
*
|
|
6
|
+
* 1. **一只铸点,三个出口**:{@link mintEngineSignal} 是「引擎此刻自己说自己什么」的唯一来源。`GET /health`(诊断面)、
|
|
7
|
+
* `GET /readyz`(就绪探针)与 fleet 心跳(`fleet-client.ts`)各读它**一次**,再按各自的出口形投影;没有出口另算一遍
|
|
8
|
+
* 就绪 / 重启 / 代际 / 租约。
|
|
9
|
+
* 2. **就绪**仍只有一只谓词(`http/readiness.ts` 的 `readinessOf`),铸点把它的判决原样带上(`verdict`)。`/readyz` 200 ⇔
|
|
10
|
+
* `ready ∧ ¬draining ∧ leaseState ≠ denied` —— 后两项不是新判据:`draining` 本来就是就绪原因,而租约被拒**同一拍**经
|
|
11
|
+
* 唯一的排空入口进入排空(`fleet/serving-lease.ts`),所以「被拒 ⇒ 排空 ⇒ 不就绪」是构造出来的,不靠第二只谓词。
|
|
12
|
+
* `leaseState = expired` **不**进就绪(中心不可达不是本实例的病,见租约模块头注)。
|
|
13
|
+
* 3. **重启**:`restart` = 已发布的重启信号;没有已发布信号而有一只**扣着**的候选(config-center 的保留候选状态机)且它确有被扣的片时,
|
|
14
|
+
* 铸 `{reasons: 被扣的片, version, since, withheld: true}`(「要重启,但此刻换掉本实例会丢它手里的候选」)。已发布的信号恒胜(那几片
|
|
15
|
+
* 是安全可重启的,扣着的片不阻止它们);被扣的片为空(只差发布重试)⇒ 不是重启事实,不铸(契约 `RestartSignal.reasons` 至少一词;
|
|
16
|
+
* 那一形的面只有每拍一行运维 warn + 计数)。「扣着」在两出口上只有这一种读法(7.120.x 的 `/health.modelPlaneDeferred` 本版删除)。
|
|
17
|
+
* 合成只在 {@link buildRestartSignal} 一处,`restartState()` 是它唯一的调用方 —— 两个出口读的是同一只取值口。
|
|
18
|
+
* 4. **代际与实例**:`generation` = env `FLEET_GENERATION` 的回声(规范十进制串;非规范在 boot 拒启,见 `config.ts`);
|
|
19
|
+
* 缺席 ⇔ 非 fleet 成员。`instanceId` = 本进程起步自铸的那一只(`boot/stage-03` 的 uuidv7),不读 env。
|
|
20
|
+
* 5. **出口方言**(契约 `EngineSignal` 与今天两出口的既有形两处不同:`draining` 是布尔、`/health` 就绪时不带 `ready` —— 契约包
|
|
21
|
+
* 自己的 `EngineSignal` 头注写明了这两处;世代账两键 settings-schema 9.0.0 起契约也是数,与引擎同形)—— 两出口**照旧**用 8.0 方言。心跳体的类型
|
|
22
|
+
* **就是**契约 `InstanceAnnouncement`(中心按它 `safeParse`;8.2.0 起它类型化 `generation` / `leaseState` / `leaseUntil`),没有
|
|
23
|
+
* 契约外的键:世代账(`configTargetVersion` / `configAppliedVersion`)契约心跳形没有座位,只上 `/health`。新增的键
|
|
24
|
+
* (`generation` / `leaseState` / `leaseUntil`、`restart.withheld`)只在 fleet 成员 / 扣着的候选在场时出现 ⇒ 非 fleet 部署的
|
|
25
|
+
* 两出口逐字节同 7.120.0。方言收口归契约包下一个 major(布尔 `draining` 对退役、两出口整只承载 `EngineSignal`)与中心提货之后,
|
|
26
|
+
* 由引擎同版翻转 —— 不在本层另造第二方言。
|
|
27
|
+
* 6. **租约四态的词表属主是契约包**(`LeaseState`):本模块的读数型 {@link ServingLeaseView} 按它逐词对齐(下面的编译期断言两向钉),
|
|
28
|
+
* 契约加一个词 ⇒ 本仓编译红,不会悄悄多出一个本仓读不出的态。
|
|
29
|
+
*
|
|
30
|
+
* 模块形:纯函数(只值 import 契约包与 `http/readiness.ts`)—— `http/` 与 `fleet-client.ts` 都读它,不许长出回环。
|
|
31
|
+
*/
|
|
32
|
+
import type { EngineDraining, InstanceAnnouncement, LeaseState, RestartSignal } from "@sema-agent/settings-schema/fleet";
|
|
33
|
+
import { type ReadinessInputs, type ReadinessVerdict } from "../http/readiness.js";
|
|
34
|
+
/**
|
|
35
|
+
* 租约此刻的读数(租约客户端交出;纯数据)。态的词表 = 契约 `LeaseState`(四词的语义在契约注里:`none` 不生效 / `granted` 持有 /
|
|
36
|
+
* `expired` 生效而此刻无有效授予 / `denied` 正面证据拒,黏性)。`pastOutageMax` = 已过期且不可达满 `T_max` ⇒ 新提交 503 `state.lease_expired`。
|
|
37
|
+
*/
|
|
38
|
+
export type ServingLeaseView = {
|
|
39
|
+
readonly state: Extract<LeaseState, "none">;
|
|
40
|
+
} | {
|
|
41
|
+
readonly state: Extract<LeaseState, "granted">;
|
|
42
|
+
readonly leaseUntil: number;
|
|
43
|
+
} | {
|
|
44
|
+
readonly state: Extract<LeaseState, "expired">;
|
|
45
|
+
readonly pastOutageMax: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
readonly state: Extract<LeaseState, "denied">;
|
|
48
|
+
};
|
|
49
|
+
/** 心跳体上的 fleet 成员三键 —— 类型取自契约心跳形(拼法与值域只有契约一个写者)。 */
|
|
50
|
+
export type FleetMembershipKeys = Pick<InstanceAnnouncement, "generation" | "leaseState" | "leaseUntil">;
|
|
51
|
+
/** 非 fleet 成员 / 没装租约客户端时的读数(唯一拼法)。 */
|
|
52
|
+
export declare const LEASE_NONE: ServingLeaseView;
|
|
53
|
+
/** 扣着的候选(config-center 的 `planeDeferredNoHandoff` 投影):版本、起扣时刻、被扣的重启片。 */
|
|
54
|
+
export interface RetainedCandidate {
|
|
55
|
+
readonly version: number;
|
|
56
|
+
readonly since: number;
|
|
57
|
+
readonly blocked?: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 重启事实的**唯一**合成点(规则 3):已发布信号恒胜;否则扣着的候选有被扣的片 ⇒ `withheld:true` 的信号;否则缺席。
|
|
61
|
+
* `build*` = 纯数据。泛型让 config-center 交进来的窄型(`EngineRestartSignal`,`reasons` 是片闭集)原样回来。
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildRestartSignal<R extends RestartSignal>(published: R | undefined, retained: (RetainedCandidate & {
|
|
64
|
+
readonly blocked?: readonly R["reasons"][number][];
|
|
65
|
+
}) | undefined): R | undefined;
|
|
66
|
+
/** `/health.configHandoff` 的状态词(闭集;今天只有一词 —— 在场即信号,缺席 = 有接力或无从谈起)。 */
|
|
67
|
+
export declare const CONFIG_HANDOFF_STATES: readonly ["missing"];
|
|
68
|
+
export type ConfigHandoffState = (typeof CONFIG_HANDOFF_STATES)[number];
|
|
69
|
+
/**
|
|
70
|
+
* `/health.configHandoff`(只在 `/health`,不上心跳)—— 一件**事故**:**在场 ⇔ 部署声明了耐久接力 ∧ 应用着的那一代没在下一次起服将用的身份的桶里确认耐久落盘**
|
|
71
|
+
* (config-center 的接力账 {@link ConfigHandoffLedger}:最后确认耐久的那一代 + 写进的那只身份桶,对比应用着的那一代 + 此刻的凭证身份 —— 离线起服按此刻
|
|
72
|
+
* 的身份选桶;部署形门在 config-center 的取值口,读判「handoff confirmed」的同一只谓词)。凭证换了人(换了身份)而新身份的桶里没有这一代 ⇒ 在场。覆盖 LKG 发布失败(含保留候选、启动期技能债)与部分应用(某组被整拒、其余组已热应用,LKG 停在上一个完整世代)两形。**部署形**不在这里报:
|
|
73
|
+
* 没有耐久声明(`CONFIG_LKG_DURABLE` 不真 / LKG 关)的部署「重启靠中心重拉」是配置时声明的事实 ⇒ 恒缺席;本地腿 / 没有配置管道 ⇒ 缺席。
|
|
74
|
+
* 与 `restart.withheld` 两个事实两张脸(可同在):要不要重启归 `restart`,声明了的接力缺不缺归本键。
|
|
75
|
+
* `version` = 应用着的那一代;`since` = 这一代开始没有确认接力的时刻(持续缺着时不刷新,确认即清)。
|
|
76
|
+
*/
|
|
77
|
+
export interface ConfigHandoff {
|
|
78
|
+
readonly state: ConfigHandoffState;
|
|
79
|
+
readonly version: number;
|
|
80
|
+
readonly since: number;
|
|
81
|
+
}
|
|
82
|
+
/** 配置接力账(config-center 持有,`/health.configHandoff` 的唯一来源):应用着的那一代、最后一次确认耐久接力的那一代与它写进的身份桶、
|
|
83
|
+
* 下一次起服将用的身份(此刻的凭证身份 —— 离线起服选桶读的同一只)。 */
|
|
84
|
+
export interface ConfigHandoffLedger {
|
|
85
|
+
readonly applied?: number;
|
|
86
|
+
readonly durable?: {
|
|
87
|
+
readonly version: number;
|
|
88
|
+
readonly identity: string;
|
|
89
|
+
};
|
|
90
|
+
readonly bootIdentity?: string;
|
|
91
|
+
}
|
|
92
|
+
/** 「接力缺着」的**唯一**判据(纯):应用过一代 ∧ 最后确认耐久的不是「这一代 ∧ 下一次起服将用的身份桶」。 */
|
|
93
|
+
export declare function configHandoffMissing(ledger: ConfigHandoffLedger): boolean;
|
|
94
|
+
/** `configHandoff` 的**唯一**合成点(纯):缺着 ⇒ `{ state: "missing", version: 应用着的那一代, since }`;否则缺席。`since` 由持账方锁存(发现缺着的那一拍)。
|
|
95
|
+
* `build*` = 纯数据;config-center 的 `configHandoffState()` 是唯一调用方。 */
|
|
96
|
+
export declare function buildConfigHandoff(ledger: ConfigHandoffLedger, since: number | undefined): ConfigHandoff | undefined;
|
|
97
|
+
/** 铸点读的输入 —— `ServiceDeps` 上的同名键(HTTP 面与心跳读的是**同一个**对象)。 */
|
|
98
|
+
export interface EngineSignalInputs extends ReadinessInputs {
|
|
99
|
+
readonly instanceId?: string;
|
|
100
|
+
/** `config.fleetGeneration` = env `FLEET_GENERATION` 的回声(boot 已判规范;缺席 ⇔ 非 fleet 成员)。 */
|
|
101
|
+
readonly config?: {
|
|
102
|
+
readonly fleetGeneration?: string;
|
|
103
|
+
};
|
|
104
|
+
readonly drainState?: ReadinessInputs["drainState"] & {
|
|
105
|
+
readonly since?: number;
|
|
106
|
+
readonly actionId?: string;
|
|
107
|
+
readonly deadlineAt?: number;
|
|
108
|
+
};
|
|
109
|
+
readonly restartState?: () => RestartSignal | undefined;
|
|
110
|
+
readonly servingLease?: () => ServingLeaseView;
|
|
111
|
+
readonly configApplyState?: () => {
|
|
112
|
+
readonly configTargetVersion: number;
|
|
113
|
+
readonly configAppliedVersion: number;
|
|
114
|
+
} | undefined;
|
|
115
|
+
}
|
|
116
|
+
/** 铸出的信号(纯数据)。 */
|
|
117
|
+
export interface EngineSignalFacts {
|
|
118
|
+
/** 就绪判(`readinessOf` 原样)。 */
|
|
119
|
+
readonly verdict: ReadinessVerdict;
|
|
120
|
+
/** 在场 ⇔ 排空中(契约 `EngineDraining`:动作 / 起点 / 期限)。 */
|
|
121
|
+
readonly draining?: EngineDraining;
|
|
122
|
+
readonly restart?: RestartSignal;
|
|
123
|
+
readonly generation?: string;
|
|
124
|
+
readonly instanceId?: string;
|
|
125
|
+
readonly lease: ServingLeaseView;
|
|
126
|
+
readonly configTargetVersion?: number;
|
|
127
|
+
readonly configAppliedVersion?: number;
|
|
128
|
+
}
|
|
129
|
+
/** **唯一**铸点(活判,每次调用现读输入)。 */
|
|
130
|
+
export declare function mintEngineSignal(deps: EngineSignalInputs): EngineSignalFacts;
|
|
131
|
+
/**
|
|
132
|
+
* fleet 成员身份三键的**唯一**投影(两出口同调,同值同序):`generation` / `leaseState` / `leaseUntil`。
|
|
133
|
+
* 在场律:非 fleet 成员(无 `generation`)⇒ 空对象(两出口逐字节同 7.120.0);成员 ⇒ `generation` + `leaseState` 恒在,
|
|
134
|
+
* `leaseUntil` 只在 `granted`。
|
|
135
|
+
*/
|
|
136
|
+
export declare function fleetMembershipKeys(s: EngineSignalFacts): FleetMembershipKeys;
|
|
137
|
+
/** 出口的 8.0 方言布尔 `draining`(心跳 `InstanceAnnouncement.draining` / `/health.draining`):就绪原因含排空。 */
|
|
138
|
+
export declare function drainingFlag(s: EngineSignalFacts): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* `/health.restartRequired`(冗余顶层布尔,壳读它决定要不要问用户重启)的投影:在场 ⇔ 有**可执行**的重启信号
|
|
141
|
+
* (`restart` 在场且未被扣)—— 与 7.120.0 的读数逐字同(那时被扣的候选根本不出 `restart`)。
|
|
142
|
+
*/
|
|
143
|
+
export declare function restartActionable(s: EngineSignalFacts): boolean;
|
|
144
|
+
//# sourceMappingURL=engine-signal.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readinessOf } from "../http/readiness.js";
|
|
2
|
+
import { drainingFactOf } from "./drain-entry.js";
|
|
3
|
+
export const LEASE_NONE = Object.freeze({ state: "none" });
|
|
4
|
+
export function buildRestartSignal(published, retained) {
|
|
5
|
+
if (published !== undefined)
|
|
6
|
+
return published;
|
|
7
|
+
if (retained === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const reasons = [...new Set(retained.blocked ?? [])];
|
|
10
|
+
if (reasons.length === 0)
|
|
11
|
+
return undefined;
|
|
12
|
+
return { reasons, version: retained.version, since: retained.since, withheld: true };
|
|
13
|
+
}
|
|
14
|
+
export const CONFIG_HANDOFF_STATES = ["missing"];
|
|
15
|
+
export function configHandoffMissing(ledger) {
|
|
16
|
+
return ledger.applied !== undefined && !(ledger.durable !== undefined && ledger.durable.version === ledger.applied && ledger.durable.identity === ledger.bootIdentity);
|
|
17
|
+
}
|
|
18
|
+
export function buildConfigHandoff(ledger, since) {
|
|
19
|
+
return configHandoffMissing(ledger) && ledger.applied !== undefined && since !== undefined ? { state: "missing", version: ledger.applied, since } : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function mintEngineSignal(deps) {
|
|
22
|
+
const verdict = readinessOf(deps);
|
|
23
|
+
const ds = deps.drainState;
|
|
24
|
+
const draining = ds === undefined ? undefined : drainingFactOf(ds);
|
|
25
|
+
const restart = deps.restartState?.();
|
|
26
|
+
const gen = deps.configApplyState?.();
|
|
27
|
+
return {
|
|
28
|
+
verdict,
|
|
29
|
+
...(draining !== undefined ? { draining } : {}),
|
|
30
|
+
...(restart !== undefined ? { restart } : {}),
|
|
31
|
+
...(deps.config?.fleetGeneration !== undefined ? { generation: deps.config.fleetGeneration } : {}),
|
|
32
|
+
...(deps.instanceId !== undefined ? { instanceId: deps.instanceId } : {}),
|
|
33
|
+
lease: deps.servingLease?.() ?? LEASE_NONE,
|
|
34
|
+
...(gen !== undefined ? { configTargetVersion: gen.configTargetVersion, configAppliedVersion: gen.configAppliedVersion } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function fleetMembershipKeys(s) {
|
|
38
|
+
if (s.generation === undefined)
|
|
39
|
+
return {};
|
|
40
|
+
const lease = s.lease;
|
|
41
|
+
switch (lease.state) {
|
|
42
|
+
case "granted":
|
|
43
|
+
return { generation: s.generation, leaseState: lease.state, leaseUntil: lease.leaseUntil };
|
|
44
|
+
case "none":
|
|
45
|
+
case "expired":
|
|
46
|
+
case "denied":
|
|
47
|
+
return { generation: s.generation, leaseState: lease.state };
|
|
48
|
+
default: {
|
|
49
|
+
const unhandled = lease;
|
|
50
|
+
throw new Error(`engine signal: unknown lease state ${JSON.stringify(unhandled)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function drainingFlag(s) {
|
|
55
|
+
return !s.verdict.ready && s.verdict.reasons.includes("draining");
|
|
56
|
+
}
|
|
57
|
+
export function restartActionable(s) {
|
|
58
|
+
return s.restart !== undefined && s.restart.withheld !== true;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=engine-signal.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CenterCapabilities } from "@sema-agent/settings-schema/fleet";
|
|
2
|
+
import { type CenterEndpoint } from "../config-center/center-request.js";
|
|
3
|
+
import { type DrainEntry } from "./drain-entry.js";
|
|
4
|
+
import type { ServingLeaseView } from "./engine-signal.js";
|
|
5
|
+
/** 续租请求的上界(ms)—— 与 fleet 心跳同量级(不在提交热路径上)。 */
|
|
6
|
+
export declare const SERVING_LEASE_FETCH_TIMEOUT_MS = 5000;
|
|
7
|
+
/** 黏性拒在数据根下的文件名。 */
|
|
8
|
+
export declare const SERVING_LEASE_DENIED_FILE = "fleet-serving-lease-denied.json";
|
|
9
|
+
export interface ServingLeaseClientOpts {
|
|
10
|
+
readonly center: CenterEndpoint & {
|
|
11
|
+
readonly worker: string;
|
|
12
|
+
};
|
|
13
|
+
readonly generation: string;
|
|
14
|
+
readonly instanceId: string;
|
|
15
|
+
/** 黏性拒落盘处(缺席 ⇒ 只在进程内黏,且按「位没落下」响亮,规则 3 末段)。 */
|
|
16
|
+
readonly dataRoot: string | undefined;
|
|
17
|
+
/** `T_max`(ms,`config.fleetLeaseOutageMaxMs`)。 */
|
|
18
|
+
readonly outageMaxMs: number;
|
|
19
|
+
/** 唯一排空入口(装配层接 `drainState.enter`)。 */
|
|
20
|
+
readonly drain: (entry: DrainEntry) => void;
|
|
21
|
+
/** 被拒那一拍的排空窗(装配层接 `config.drainGraceMs` —— 期限在进入排空那一拍定死)。 */
|
|
22
|
+
readonly drainWindowMs: () => number;
|
|
23
|
+
readonly fetchImpl?: typeof fetch;
|
|
24
|
+
readonly logger?: {
|
|
25
|
+
info: (m: string, d?: Record<string, unknown>) => void;
|
|
26
|
+
warn: (m: string, d?: Record<string, unknown>) => void;
|
|
27
|
+
error: (m: string, d?: Record<string, unknown>) => void;
|
|
28
|
+
};
|
|
29
|
+
/** 测试注入:单调钟 / 挂钟(缺省 `performance.now` / `Date.now`)。 */
|
|
30
|
+
readonly clock?: {
|
|
31
|
+
readonly mono: () => number;
|
|
32
|
+
readonly epoch: () => number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface ServingLeaseClient {
|
|
36
|
+
/** 一次中心能力读数(`undefined` = `undeclared`,中心没宣告 = 不支持)。每一次读数调一次;`unreadable` 不是读数,不调(`fleet-client.ts` `capabilityReadingOf`)。 */
|
|
37
|
+
noteCenterCapabilities(caps: CenterCapabilities | undefined): void;
|
|
38
|
+
/** 此刻的租约读数(铸点与准入判读它)。 */
|
|
39
|
+
view(): ServingLeaseView;
|
|
40
|
+
/** 起步读黏性拒(装配层在唯一排空入口装好之后调一次)。 */
|
|
41
|
+
resumeStickyDenial(): void;
|
|
42
|
+
/** 立刻续一次(测试 / 能力位刚出现时用;单飞)。 */
|
|
43
|
+
renewNow(): Promise<void>;
|
|
44
|
+
stop(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function createServingLeaseClient(o: ServingLeaseClientOpts): ServingLeaseClient;
|
|
47
|
+
//# sourceMappingURL=serving-lease.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SERVING_LEASE_TTL_DEFAULT_MS, readServingLeaseResponse } from "@sema-agent/settings-schema/fleet";
|
|
4
|
+
import { centerFetch } from "../config-center/center-request.js";
|
|
5
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
6
|
+
import { leaseDrainActionId } from "./drain-entry.js";
|
|
7
|
+
export const SERVING_LEASE_FETCH_TIMEOUT_MS = 5_000;
|
|
8
|
+
export const SERVING_LEASE_DENIED_FILE = "fleet-serving-lease-denied.json";
|
|
9
|
+
const RENEW_MIN_INTERVAL_MS = 1_000;
|
|
10
|
+
function denialReasonOf(d) {
|
|
11
|
+
switch (d.from) {
|
|
12
|
+
case "center":
|
|
13
|
+
case "data-root":
|
|
14
|
+
return d.reason;
|
|
15
|
+
case "data-root-unreadable":
|
|
16
|
+
return "unreadable_record";
|
|
17
|
+
default: {
|
|
18
|
+
const unhandled = d;
|
|
19
|
+
throw new Error(`serving lease: unknown denial source ${JSON.stringify(unhandled)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function denialLeadOf(d) {
|
|
24
|
+
switch (d.from) {
|
|
25
|
+
case "center":
|
|
26
|
+
return `the fleet center denied this instance's serving lease (${d.reason})`;
|
|
27
|
+
case "data-root":
|
|
28
|
+
return `the fleet center denied this generation's serving lease earlier (${d.reason}) and the data root remembers it`;
|
|
29
|
+
case "data-root-unreadable":
|
|
30
|
+
return "the persisted lease record could not be read; treated as denied (fail-closed) — delete the record only if this generation is still meant to serve";
|
|
31
|
+
default: {
|
|
32
|
+
const unhandled = d;
|
|
33
|
+
throw new Error(`serving lease: unknown denial source ${JSON.stringify(unhandled)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function createServingLeaseClient(o) {
|
|
38
|
+
const mono = o.clock?.mono ?? (() => performance.now());
|
|
39
|
+
const epoch = o.clock?.epoch ?? Date.now;
|
|
40
|
+
const f = o.fetchImpl ?? fetch;
|
|
41
|
+
const stickyPath = o.dataRoot === undefined ? undefined : join(o.dataRoot, SERVING_LEASE_DENIED_FILE);
|
|
42
|
+
let enabled = false;
|
|
43
|
+
let enabledAtMono = 0;
|
|
44
|
+
let denied = false;
|
|
45
|
+
let stopped = false;
|
|
46
|
+
let lease;
|
|
47
|
+
let nextInMs = SERVING_LEASE_TTL_DEFAULT_MS / 3;
|
|
48
|
+
let timer;
|
|
49
|
+
let inFlight;
|
|
50
|
+
let failStreak = 0;
|
|
51
|
+
const clear = () => {
|
|
52
|
+
if (timer !== undefined)
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
timer = undefined;
|
|
55
|
+
};
|
|
56
|
+
const schedule = (ms) => {
|
|
57
|
+
clear();
|
|
58
|
+
if (stopped || denied || !enabled)
|
|
59
|
+
return;
|
|
60
|
+
timer = setTimeout(() => void renewNow(), Math.max(RENEW_MIN_INTERVAL_MS, ms));
|
|
61
|
+
timer.unref?.();
|
|
62
|
+
};
|
|
63
|
+
const persistDenial = (reason) => {
|
|
64
|
+
if (stickyPath === undefined) {
|
|
65
|
+
o.logger?.error("fleet_serving_lease_denial_not_persisted", { note: "no data root to remember the denial in — it holds in this process only; after a restart this instance serves until its first renewal is answered" });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const record = { worker: o.center.worker, generation: o.generation, reason, at: epoch() };
|
|
69
|
+
const tmp = `${stickyPath}.tmp-${process.pid}`;
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
72
|
+
renameSync(tmp, stickyPath);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
o.logger?.error("fleet_serving_lease_denial_not_persisted", { path: stickyPath, error: err instanceof Error ? err.message : String(err), note: "the denial holds in this process only; after a restart this instance serves until its first renewal is answered (the center denies again then)" });
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const enterDenied = (denial) => {
|
|
81
|
+
if (denied)
|
|
82
|
+
return;
|
|
83
|
+
denied = true;
|
|
84
|
+
lease = undefined;
|
|
85
|
+
clear();
|
|
86
|
+
const reason = denialReasonOf(denial);
|
|
87
|
+
const sticky = denial.from !== "center" ? "from-data-root" : persistDenial(reason) ? "new" : "process-only";
|
|
88
|
+
if (sticky === "process-only")
|
|
89
|
+
recordFailOpen("server.fleet.denied-marker-unpersisted", `worker=${o.center.worker} generation=${o.generation}`);
|
|
90
|
+
o.logger?.warn("fleet_serving_lease_denied", {
|
|
91
|
+
generation: o.generation,
|
|
92
|
+
worker: o.center.worker,
|
|
93
|
+
reason,
|
|
94
|
+
sticky,
|
|
95
|
+
note: `${denialLeadOf(denial)} — draining now and stopping at the drain deadline; ${sticky === "process-only" ? "the denial could NOT be recorded in the data root, so a restart of this generation does not remember it" : "the denial is remembered across restarts of this generation"}`,
|
|
96
|
+
});
|
|
97
|
+
o.drain({ actionId: leaseDrainActionId(o.generation), deadlineAt: epoch() + o.drainWindowMs() });
|
|
98
|
+
};
|
|
99
|
+
const renewOnce = async () => {
|
|
100
|
+
if (stopped || denied || !enabled)
|
|
101
|
+
return;
|
|
102
|
+
const sendMono = mono();
|
|
103
|
+
const sendEpoch = epoch();
|
|
104
|
+
const request = { worker: o.center.worker, generation: o.generation, instanceId: o.instanceId };
|
|
105
|
+
let reading;
|
|
106
|
+
let why = "";
|
|
107
|
+
try {
|
|
108
|
+
const { res } = await centerFetch(o.center, { kind: "fleet-serving-lease" }, { method: "POST", json: request, timeoutMs: SERVING_LEASE_FETCH_TIMEOUT_MS, fetchImpl: f });
|
|
109
|
+
if (res.ok) {
|
|
110
|
+
reading = readServingLeaseResponse(await res.json().catch(() => undefined));
|
|
111
|
+
if (reading === undefined)
|
|
112
|
+
why = "reply is not a serving-lease verdict";
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
why = `HTTP ${res.status}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
why = err instanceof Error ? err.message : String(err);
|
|
120
|
+
}
|
|
121
|
+
if (reading === undefined || reading.verdict === "unknown") {
|
|
122
|
+
const unconfirmed = reading === undefined ? why : "verdict=unknown (the center holds no lifecycle object for this instance right now)";
|
|
123
|
+
recordFailOpen("server.fleet.serving-lease-unconfirmed", `worker=${o.center.worker} generation=${o.generation} why=${unconfirmed}`);
|
|
124
|
+
failStreak += 1;
|
|
125
|
+
if (failStreak === 1)
|
|
126
|
+
o.logger?.warn("fleet_serving_lease_unconfirmed", { error: unconfirmed, note: "the serving lease could not be renewed (the center is unreachable or did not confirm this instance) — this instance keeps serving until the lease expires and then for FLEET_LEASE_OUTAGE_MAX_MS more before it refuses new work (unconfirmed is never a denial)" });
|
|
127
|
+
schedule(nextInMs);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (failStreak > 0)
|
|
131
|
+
o.logger?.info("fleet_serving_lease_recovered", { after: failStreak });
|
|
132
|
+
failStreak = 0;
|
|
133
|
+
switch (reading.verdict) {
|
|
134
|
+
case "granted":
|
|
135
|
+
if (denied || !enabled)
|
|
136
|
+
return;
|
|
137
|
+
lease = { expiresAtMono: sendMono + reading.ttlMs, leaseUntil: sendEpoch + reading.ttlMs };
|
|
138
|
+
nextInMs = reading.ttlMs / 3;
|
|
139
|
+
schedule(nextInMs);
|
|
140
|
+
return;
|
|
141
|
+
case "denied":
|
|
142
|
+
enterDenied({ from: "center", reason: reading.reason });
|
|
143
|
+
return;
|
|
144
|
+
default: {
|
|
145
|
+
const unhandled = reading;
|
|
146
|
+
throw new Error(`serving lease: unknown verdict ${JSON.stringify(unhandled)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const renewNow = () => {
|
|
151
|
+
if (inFlight !== undefined)
|
|
152
|
+
return inFlight;
|
|
153
|
+
inFlight = renewOnce().finally(() => {
|
|
154
|
+
inFlight = undefined;
|
|
155
|
+
});
|
|
156
|
+
return inFlight;
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
noteCenterCapabilities(caps) {
|
|
160
|
+
if (stopped || denied)
|
|
161
|
+
return;
|
|
162
|
+
const on = caps?.servingLease === true;
|
|
163
|
+
if (!on)
|
|
164
|
+
recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled ? "enabled" : "none"}`);
|
|
165
|
+
if (on && !enabled) {
|
|
166
|
+
enabled = true;
|
|
167
|
+
enabledAtMono = mono();
|
|
168
|
+
o.logger?.info("fleet_serving_lease_enabled", { generation: o.generation, worker: o.center.worker });
|
|
169
|
+
void renewNow();
|
|
170
|
+
}
|
|
171
|
+
else if (!on && enabled) {
|
|
172
|
+
enabled = false;
|
|
173
|
+
lease = undefined;
|
|
174
|
+
clear();
|
|
175
|
+
o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again" });
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
view() {
|
|
179
|
+
if (denied)
|
|
180
|
+
return { state: "denied" };
|
|
181
|
+
if (!enabled)
|
|
182
|
+
return { state: "none" };
|
|
183
|
+
const now = mono();
|
|
184
|
+
if (lease !== undefined && now < lease.expiresAtMono)
|
|
185
|
+
return { state: "granted", leaseUntil: lease.leaseUntil };
|
|
186
|
+
const expiredAt = lease?.expiresAtMono ?? enabledAtMono;
|
|
187
|
+
return { state: "expired", pastOutageMax: now - expiredAt >= o.outageMaxMs };
|
|
188
|
+
},
|
|
189
|
+
resumeStickyDenial() {
|
|
190
|
+
if (stickyPath === undefined || denied)
|
|
191
|
+
return;
|
|
192
|
+
let text;
|
|
193
|
+
try {
|
|
194
|
+
text = readFileSync(stickyPath, "utf8");
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err.code === "ENOENT")
|
|
198
|
+
return;
|
|
199
|
+
o.logger?.error("fleet_serving_lease_denial_unreadable", { path: stickyPath, error: err instanceof Error ? err.message : String(err), note: "a remembered lease denial could not be read — treating this instance as denied (fail-closed); delete the file only if this generation is still meant to serve" });
|
|
200
|
+
enterDenied({ from: "data-root-unreadable" });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let parsed;
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(text);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
parsed = undefined;
|
|
209
|
+
}
|
|
210
|
+
const record = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
|
|
211
|
+
if (record === undefined || typeof record.generation !== "string" || typeof record.worker !== "string") {
|
|
212
|
+
o.logger?.error("fleet_serving_lease_denial_unreadable", { path: stickyPath, note: "a remembered lease denial is malformed — treating this instance as denied (fail-closed); delete the file only if this generation is still meant to serve" });
|
|
213
|
+
enterDenied({ from: "data-root-unreadable" });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (record.worker !== o.center.worker || record.generation !== o.generation) {
|
|
217
|
+
o.logger?.info("fleet_serving_lease_denial_stale", { path: stickyPath, recorded: { worker: record.worker, generation: record.generation }, current: { worker: o.center.worker, generation: o.generation } });
|
|
218
|
+
try {
|
|
219
|
+
rmSync(stickyPath, { force: true });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
o.logger?.error("fleet_serving_lease_denial_stale_unremovable", { path: stickyPath, error: err instanceof Error ? err.message : String(err), note: "a stale lease denial of another generation could not be removed — it does not bind this generation; remove it by hand" });
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
enterDenied({ from: "data-root", reason: typeof record.reason === "string" ? record.reason : "unknown_reason" });
|
|
227
|
+
},
|
|
228
|
+
renewNow,
|
|
229
|
+
stop() {
|
|
230
|
+
stopped = true;
|
|
231
|
+
clear();
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=serving-lease.js.map
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* `/fleet`,center 端点 = sema-registry `/api/fleet/*`)。设计要点:
|
|
4
4
|
* - **可选分层(D3),两条环各自有门(S-866 ④)**:客户端在中心车道配齐时启动(`centerLaneConfigured` —— baseUrl + 声明了
|
|
5
5
|
* 凭证 + worker,与租约管理器、用量累加器**同一只**谓词);**用量环**只要这三件;**公告环**(announce / 心跳 / 注销)另要
|
|
6
|
-
* `FLEET_ADVERTISE_ADDRESS`
|
|
7
|
-
*
|
|
6
|
+
* 配置席 `fleetAdvertiseAddress`(env `FLEET_ADVERTISE_ADDRESS`,`loadConfig` 解析一次;可达性是运维的话,不自猜:k8s pod IP /
|
|
7
|
+
* docker alias / 127.0.0.1:port 只有部署环境自己知道)。不配中心的部署零行为变化;配了中心而不公告的引擎只报用量、不注册(7.105.0 及以前它连用量都不报)。
|
|
8
8
|
* - **announce=注册=心跳同形状幂等 upsert**:boot 后(listen 之后)POST 一次 + 每 heartbeatMs 续。
|
|
9
9
|
* `ready` / `draining` 两字段由**同一只**就绪谓词的同一次读铸(S-740:`http/readiness.ts` 的 `readinessOf`,
|
|
10
10
|
* 与 `/health.ready`、`GET /readyz`、计费提交门同源):`ready = verdict.ready`(= roster 已落 ∧ ¬draining),
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
* 向前合并不会双计 —— 只延迟,不丢量)。
|
|
26
26
|
* - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
|
|
27
27
|
*/
|
|
28
|
-
import { UsageReport, type
|
|
29
|
-
import type
|
|
28
|
+
import { UsageReport, type CapabilityReading, type CenterCapabilities, type ModelRefusalsSignal } from "@sema-agent/settings-schema/fleet";
|
|
29
|
+
import { type EngineSignalFacts } from "./fleet/engine-signal.js";
|
|
30
30
|
import { type CenterEndpoint } from "./config-center/center-request.js";
|
|
31
31
|
/** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
|
|
32
32
|
* `__other__` instead of growing unbounded (same posture as budget.ts cardinalityGuard). */
|
|
@@ -79,15 +79,17 @@ export interface FleetClientOpts {
|
|
|
79
79
|
* S-866 ④:**缺席 = 公告环不装**(不注册、不心跳、不注销;`announceNow` 是空操作),用量环照装。 */
|
|
80
80
|
advertiseAddress?: string | undefined;
|
|
81
81
|
version: string;
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* `restart` 键。必填:心跳面没有「不知道要不要重启」这一形。 */
|
|
87
|
-
restart: () => RestartSignal | undefined;
|
|
82
|
+
/** 编排器片 2 件 1:引擎信号的**一只铸点**(装配层接 `mintEngineSignal(deployment)` —— `/health` 与 `/readyz` 读的同一只、同一个 deps 组)。
|
|
83
|
+
* 每条公告现铸**一次**,`ready` / `draining` / `restart` / 代际与租约三键 / 世代账都取自这一次(不会撕裂成 `ready:true` + `draining:true`,
|
|
84
|
+
* 也不会出现心跳与 `/health` 各算一份重启或租约)。修前是 `readiness` + `restart` 两只取值口(S-740 / S-741),合进这一只。 */
|
|
85
|
+
signal: () => EngineSignalFacts;
|
|
88
86
|
/** S-768:模型拒因信号(装配层接 `/health` 同一只取值口 `deployment.modelRefusalsState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
89
87
|
* `modelRefusals` 键。必填:与 `restart` 同 —— 心跳面没有「不知道有没有拒」这一形(老引擎不发这个键,中心按 `version` 判「不可知」)。 */
|
|
90
88
|
modelRefusals: () => ModelRefusalsSignal | undefined;
|
|
89
|
+
/** 编排器片 2:每一次**能力读数**交出去一次(装配层接服务租约客户端的 `noteCenterCapabilities`)。规则见 {@link capabilityReadingOf}:
|
|
90
|
+
* 只有 `declared`(交出宣告的位)与 `undeclared`(交 `undefined` = 老中心没宣告)是读数;`unreadable`(体读不出 / 不是对象 /
|
|
91
|
+
* `capabilities` 键在场而形坏)不是读数 ⇒ 不调,上一次读数与它的钟原样。失败的公告同样不调(没听到 ≠ 没有)。 */
|
|
92
|
+
onCenterCapabilities?: (caps: CenterCapabilities | undefined) => void;
|
|
91
93
|
usage?: FleetUsageAccumulator;
|
|
92
94
|
heartbeatMs?: number;
|
|
93
95
|
usageWindowMs?: number;
|
|
@@ -114,25 +116,39 @@ export interface FleetClient {
|
|
|
114
116
|
flushFinalUsage: () => Promise<void>;
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
|
-
*
|
|
119
|
+
* 心跳 2xx 答复读不成能力读数的原因 —— 一张闭集:`body` = 答复体本身读不出(空体 / 截断 / 非 JSON),其余两词 = 契约读器
|
|
120
|
+
* `readCapabilityReading` 的 `unreadable` 两因(`not-an-object` / `invalid-capabilities`)。
|
|
121
|
+
*/
|
|
122
|
+
export type AnnounceReplyFlaw = "body" | Extract<CapabilityReading, {
|
|
123
|
+
kind: "unreadable";
|
|
124
|
+
}>["why"];
|
|
125
|
+
/** 本仓对一次心跳答复的读法:契约三态,`unreadable` 的原因扩成 {@link AnnounceReplyFlaw}(`detail` = 读不出体时的那句错误,当数据交出)。 */
|
|
126
|
+
export type AnnounceReplyReading = Exclude<CapabilityReading, {
|
|
127
|
+
kind: "unreadable";
|
|
128
|
+
}> | {
|
|
129
|
+
readonly kind: "unreadable";
|
|
130
|
+
readonly why: AnnounceReplyFlaw;
|
|
131
|
+
readonly detail?: string;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Start the worker-side fleet loops. Caller has already gated on the center lane (startFleetClientFromConfig below).
|
|
118
135
|
* The announce loop runs iff `advertiseAddress` is present; the usage loop iff `usage` is present (S-866 ④: the two
|
|
119
136
|
* loops no longer share one gate).
|
|
120
137
|
*/
|
|
121
138
|
export declare function startFleetClient(opts: FleetClientOpts): FleetClient;
|
|
122
139
|
/**
|
|
123
|
-
*
|
|
140
|
+
* Config gate (D3 可选分层;S-866 ④ 两条环各自有门): start when the center lane is configured —
|
|
124
141
|
* {@link centerLaneConfigured}(baseUrl + a DECLARED credential + worker — S-651: declared, not "currently readable": a token
|
|
125
142
|
* file that is unreadable right now still starts the client, every request re-reads it), the SAME predicate the lease
|
|
126
143
|
* manager and the usage accumulator use. The usage loop needs nothing more; the announce loop additionally needs the
|
|
127
|
-
* operator's explicit reachable address
|
|
128
|
-
*
|
|
129
|
-
* (half-configured announce).
|
|
144
|
+
* operator's explicit reachable address — the config seat `fleetAdvertiseAddress` (env `FLEET_ADVERTISE_ADDRESS`, parsed once by
|
|
145
|
+
* `loadConfig`: a malformed one refuses boot there). Lane not configured → undefined (zero behavior change for non-fleet
|
|
146
|
+
* deployments); an address set while only the worker is missing is warned (half-configured announce).
|
|
130
147
|
*/
|
|
131
|
-
export declare function
|
|
148
|
+
export declare function startFleetClientFromConfig(cfg: {
|
|
132
149
|
configCenter?: (CenterEndpoint & {
|
|
133
150
|
worker?: string;
|
|
134
151
|
}) | undefined;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}): FleetClient | undefined;
|
|
152
|
+
fleetAdvertiseAddress?: string | undefined;
|
|
153
|
+
}, opts: Omit<FleetClientOpts, "center" | "advertiseAddress">): FleetClient | undefined;
|
|
138
154
|
//# sourceMappingURL=fleet-client.d.ts.map
|