@sema-agent/server 7.120.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 +75 -0
- package/USAGE.md +67 -20
- 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 +33 -9
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +54 -16
- 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/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/config-catalog.js +11 -5
- package/dist/config-center/center-request.d.ts +16 -1
- package/dist/config-center/center-request.js +36 -5
- package/dist/config-types.d.ts +39 -3
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +33 -4
- package/dist/config.js +99 -7
- 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 +1 -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 +2 -1
- package/dist/http/routes/devices.js +3 -2
- 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 +4 -3
- 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/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/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/plugins/one-time-migrations.js +1 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
- package/dist/plugins/remote-env-device.js +0 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/rules-consent.d.ts +6 -2
- package/dist/rules-consent.js +3 -0
- package/dist/runs.js +1 -1
- 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/self-minted-wire.d.ts +64 -0
- package/dist/trace/self-minted-wire.js +30 -0
- package/package.json +2 -2
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Model, type PathRuleBases, type PermissionRule } from "@sema-agent/core";
|
|
2
|
+
import { type CenterEndpoint } from "./config-center/center-request.js";
|
|
2
3
|
import { type Autonomy } from "./runtime-governance.js";
|
|
3
4
|
import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
|
|
4
5
|
import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
|
|
@@ -243,10 +244,7 @@ export declare function configKnobTable(): ConfigKnobRecord[];
|
|
|
243
244
|
/** [ref] B4 ②:D 族四枚(`PROJECT_MEMORY` / `CONFIG_LKG` / `HOST_BG` / `HOST_EXEC_SPOOL`)的正名 —— 缺省开,读点只读正名。
|
|
244
245
|
* 3.0.0 之前它们叫 `X_DISABLED`(读点上的双重否定);那四个旧负名是墓碑,在 `RETIRED_ENVS` 里(S-588 起在场即拒启,不论正名在不在)。
|
|
245
246
|
*
|
|
246
|
-
*
|
|
247
|
-
* rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
|
|
248
|
-
* so its polarity row appears at boot with everything else. */
|
|
249
|
-
export declare const configLkgEnabled: () => boolean;
|
|
247
|
+
* 其中 `CONFIG_LKG_ENABLED`(远程配置的 LKG 盘缓存)已升成配置席 `configLkgEnabled`,由 `loadConfig` 一次铸出;这里只剩另外两个懒读孪生。 */
|
|
250
248
|
/** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
|
|
251
249
|
export declare const hostBackgroundShellEnabled: () => boolean;
|
|
252
250
|
/** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
|
|
@@ -402,6 +400,37 @@ export declare const LIMITS_HTTP_FLOORS: {
|
|
|
402
400
|
readonly attachmentUnboundTtlMs: 60000;
|
|
403
401
|
readonly drainGraceMs: 10000;
|
|
404
402
|
};
|
|
403
|
+
/**
|
|
404
|
+
* 编排器片 2 —— fleet 成员面 env 的拒启码(拼法随 `config.*` 家族,按**规则**命名)。拒启异常把码挂在 `code` 上(`boot/fatal-exit.ts` 按
|
|
405
|
+
* `<name> [<code>]: <message>` 印进 fatal 标题),句子本身不再重复方括号。闭集:加一种 = 加一个键。
|
|
406
|
+
*/
|
|
407
|
+
export declare const FLEET_ENV_REFUSAL_CODES: {
|
|
408
|
+
readonly generation_invalid: "config.fleet_generation_invalid";
|
|
409
|
+
readonly scope_invalid: "config.fleet_scope_invalid";
|
|
410
|
+
readonly advertise_address_invalid: "config.fleet_advertise_address_invalid";
|
|
411
|
+
readonly member_unfenced: "config.fleet_member_unfenced";
|
|
412
|
+
};
|
|
413
|
+
export type FleetEnvRefusalCode = (typeof FLEET_ENV_REFUSAL_CODES)[keyof typeof FLEET_ENV_REFUSAL_CODES];
|
|
414
|
+
/** 一只 fleet 成员的围栏车道:续租打的中心端点(车道对象原样,含活性基址)+ 代际。 */
|
|
415
|
+
export interface FleetFenceLane {
|
|
416
|
+
readonly center: CenterEndpoint & {
|
|
417
|
+
readonly worker: string;
|
|
418
|
+
};
|
|
419
|
+
readonly generation: string;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 编排器片 2 规则 0(成员 ⇒ 围栏车道必须齐)的**唯一**判官:非成员(`fleetGeneration` 缺席)⇒ `undefined`;成员 ∧ 中心车道齐
|
|
423
|
+
* (`centerLaneConfigured`)∧ 公告地址在 ⇒ 车道;成员而缺任一件 ⇒ 拒启(点名缺的键)。`loadConfig` 在 integrations 域里调一次(拒启早于
|
|
424
|
+
* 任何装配);装配层(第 10 段)对同一份配置再调一次取车道值 —— 同一只判官,过了 `loadConfig` 的配置在那里不会再拒。
|
|
425
|
+
* 读成「非成员」或「装不上就算了」会让一只本该受围栏的实例悄悄脱离围栏:能力位只在心跳答复里,不公告就永远听不到。
|
|
426
|
+
*/
|
|
427
|
+
export declare function fleetFenceLaneOf(cfg: {
|
|
428
|
+
readonly fleetGeneration?: string | undefined;
|
|
429
|
+
readonly configCenter?: (CenterEndpoint & {
|
|
430
|
+
readonly worker?: string | undefined;
|
|
431
|
+
}) | undefined;
|
|
432
|
+
readonly fleetAdvertiseAddress?: string | undefined;
|
|
433
|
+
}): FleetFenceLane | undefined;
|
|
405
434
|
/** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
|
|
406
435
|
export declare const CONFIG_GROUP_KEYS: {
|
|
407
436
|
readonly [G in keyof ServiceConfigGroups]: readonly (keyof ServiceConfigFlat)[];
|
package/dist/config.js
CHANGED
|
@@ -10,7 +10,10 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
|
10
10
|
import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } from "./memory-scope.js";
|
|
11
11
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
12
12
|
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
|
|
13
|
-
import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
|
|
13
|
+
import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, fleetScopeOf } from "./config-types.js";
|
|
14
|
+
import { FLEET_GENERATION_ENV, FLEET_SCOPE_ENV, SERVING_LEASE_TTL_FLOOR_MS, isCanonicalGeneration } from "@sema-agent/settings-schema/fleet";
|
|
15
|
+
import { ScopeId } from "@sema-agent/settings-schema/api/scopes";
|
|
16
|
+
import { centerLaneConfigured, pathBaseUrlFlaw } from "./config-center/center-request.js";
|
|
14
17
|
import { resolveLocalDataRoot } from "./local-data-root.js";
|
|
15
18
|
import { buildWriteProtectionPosture } from "./write-protection.js";
|
|
16
19
|
import { normalizeSshFingerprint } from "./ssh-host-key.js";
|
|
@@ -488,7 +491,6 @@ function boolEnv(name, def) {
|
|
|
488
491
|
registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
|
|
489
492
|
return value;
|
|
490
493
|
}
|
|
491
|
-
export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
|
|
492
494
|
export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
|
|
493
495
|
export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
|
|
494
496
|
function parseReadDenyPatternsEnv(raw) {
|
|
@@ -1853,7 +1855,6 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1853
1855
|
const workflowSizeGuideline = workflowSizeGuidelineRaw;
|
|
1854
1856
|
const usageWindows = parseUsageWindows(process.env.USAGE_WINDOWS);
|
|
1855
1857
|
const bakeRunnerPrincipal = env("BAKE_RUNNER_PRINCIPAL", "system:bake-runner");
|
|
1856
|
-
configLkgEnabled();
|
|
1857
1858
|
hostBackgroundShellEnabled();
|
|
1858
1859
|
hostExecSpoolEnabled();
|
|
1859
1860
|
const lspHostEnabled = (() => {
|
|
@@ -1898,6 +1899,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1898
1899
|
})(),
|
|
1899
1900
|
...((dir) => (dir !== undefined ? { agentConfigDir: dir } : {}))(declaredSemaConfigDir()),
|
|
1900
1901
|
...((p) => (p !== undefined ? { configLkgPath: p } : {}))(lkgPathOverrideOf(process.env.CONFIG_LKG_PATH)),
|
|
1902
|
+
configLkgEnabled: boolEnv("CONFIG_LKG_ENABLED", true),
|
|
1901
1903
|
...((h) => (h !== undefined ? { remoteExecHomeDir: h } : {}))((() => {
|
|
1902
1904
|
const raw = process.env.REMOTE_EXEC_HOME_DIR;
|
|
1903
1905
|
if (raw === undefined || raw === "")
|
|
@@ -2186,6 +2188,13 @@ function parseGitApiKind(raw, baseUrl) {
|
|
|
2186
2188
|
}
|
|
2187
2189
|
function parseIntegrationsDomain(ctx) {
|
|
2188
2190
|
const { requirePrincipal, configProvider } = ctx;
|
|
2191
|
+
const registryUrl = parseCenterBaseUrl("SEMA_REGISTRY_URL", process.env.SEMA_REGISTRY_URL, `It is the center base every config pull, fleet and lease request is sent to; setting it selects the center as the config source.`);
|
|
2192
|
+
const registryLiveUrl = parseCenterBaseUrl("SEMA_REGISTRY_LIVE_URL", process.env.SEMA_REGISTRY_LIVE_URL, `It is the center base the fleet liveness endpoints (heartbeat, serving lease) are sent to; unset it to send them to SEMA_REGISTRY_URL.`);
|
|
2193
|
+
const registryWebUrl = parseCenterBaseUrl("SEMA_REGISTRY_WEB_URL", process.env.SEMA_REGISTRY_WEB_URL, `It is the center's web console base this engine reports on GET /health (center.webBaseUrl); unset it if the center has no web console to link to.`);
|
|
2194
|
+
const configCenter = registryUrl !== undefined ? parseConfigCenter(registryUrl, requirePrincipal, registryLiveUrl, registryWebUrl) : undefined;
|
|
2195
|
+
const fleetGeneration = parseFleetGeneration(process.env.FLEET_GENERATION);
|
|
2196
|
+
const fleetAdvertiseAddress = parseFleetAdvertiseAddress(process.env.FLEET_ADVERTISE_ADDRESS);
|
|
2197
|
+
fleetFenceLaneOf({ ...fleetGeneration, configCenter, ...fleetAdvertiseAddress });
|
|
2189
2198
|
return {
|
|
2190
2199
|
pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
|
|
2191
2200
|
configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
|
|
@@ -2195,24 +2204,107 @@ function parseIntegrationsDomain(ctx) {
|
|
|
2195
2204
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
2196
2205
|
skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
|
|
2197
2206
|
centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
|
|
2198
|
-
configCenter
|
|
2207
|
+
configCenter,
|
|
2199
2208
|
fleetLeaseFailClosed: parseFleetLeaseFailClosed(process.env.FLEET_LEASE_FAIL_CLOSED, requirePrincipal),
|
|
2209
|
+
...fleetGeneration,
|
|
2210
|
+
fleetScope: parseFleetScope(process.env.FLEET_SCOPE),
|
|
2211
|
+
...fleetAdvertiseAddress,
|
|
2212
|
+
fleetLeaseOutageMaxMs: parseFleetLeaseOutageMaxMs(process.env.FLEET_LEASE_OUTAGE_MAX_MS),
|
|
2200
2213
|
...(configProvider !== undefined ? { configProvider } : {}),
|
|
2201
2214
|
configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
|
|
2202
2215
|
a2aServe: parseA2aServe(),
|
|
2203
2216
|
};
|
|
2204
2217
|
}
|
|
2218
|
+
function parseFleetGeneration(raw) {
|
|
2219
|
+
const name = "FLEET_GENERATION";
|
|
2220
|
+
if (raw === undefined)
|
|
2221
|
+
return {};
|
|
2222
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
2223
|
+
if (!isCanonicalGeneration(raw)) {
|
|
2224
|
+
throw fleetEnvRefusal("generation_invalid", `env ${name}=${JSON.stringify(shown)} is not a generation: it must be a canonical non-negative decimal string ("0", "7", "10" — no leading zero, ` +
|
|
2225
|
+
`sign, whitespace or fraction, and not empty). The config renderer sets it on fleet members; unset it entirely for a standalone engine. ` +
|
|
2226
|
+
`Refusing to boot rather than running outside the serving-lease fence.`);
|
|
2227
|
+
}
|
|
2228
|
+
return { fleetGeneration: raw };
|
|
2229
|
+
}
|
|
2230
|
+
export const FLEET_ENV_REFUSAL_CODES = {
|
|
2231
|
+
generation_invalid: "config.fleet_generation_invalid",
|
|
2232
|
+
scope_invalid: "config.fleet_scope_invalid",
|
|
2233
|
+
advertise_address_invalid: "config.fleet_advertise_address_invalid",
|
|
2234
|
+
member_unfenced: "config.fleet_member_unfenced",
|
|
2235
|
+
};
|
|
2236
|
+
function fleetEnvRefusal(rule, message) {
|
|
2237
|
+
return Object.assign(new Error(message), { code: FLEET_ENV_REFUSAL_CODES[rule] });
|
|
2238
|
+
}
|
|
2239
|
+
function parseFleetScope(raw) {
|
|
2240
|
+
const name = "FLEET_SCOPE";
|
|
2241
|
+
if (raw !== undefined && !ScopeId.safeParse(raw).success) {
|
|
2242
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
2243
|
+
throw fleetEnvRefusal("scope_invalid", `env ${name}=${JSON.stringify(shown)} is not a scope id: it must be a lowercase slug of letters, digits and inner dashes, 1–64 characters, ` +
|
|
2244
|
+
`not empty. The config renderer sets it on engines that serve a tenant scope; unset it for the default scope ("${fleetScopeOf(undefined)}"). ` +
|
|
2245
|
+
`Refusing to boot rather than answering drain requests under the wrong identity.`);
|
|
2246
|
+
}
|
|
2247
|
+
return fleetScopeOf(raw);
|
|
2248
|
+
}
|
|
2249
|
+
function parseFleetAdvertiseAddress(raw) {
|
|
2250
|
+
if (raw === undefined || raw === "")
|
|
2251
|
+
return {};
|
|
2252
|
+
const why = pathBaseUrlFlaw(raw);
|
|
2253
|
+
if (why !== undefined) {
|
|
2254
|
+
throw fleetEnvRefusal("advertise_address_invalid", `env FLEET_ADVERTISE_ADDRESS ${why} (an http(s) base URL, e.g. "http://10.0.0.5:8090"); got ${JSON.stringify(buildDisplaySafeUrl(raw))} — ` +
|
|
2255
|
+
`the fleet center rejects any other shape on every announce. Unset it if this engine does not announce itself.`);
|
|
2256
|
+
}
|
|
2257
|
+
return { fleetAdvertiseAddress: raw };
|
|
2258
|
+
}
|
|
2259
|
+
export function fleetFenceLaneOf(cfg) {
|
|
2260
|
+
const generation = cfg.fleetGeneration;
|
|
2261
|
+
if (generation === undefined)
|
|
2262
|
+
return undefined;
|
|
2263
|
+
const laneConfigured = centerLaneConfigured(cfg);
|
|
2264
|
+
if (laneConfigured && cfg.fleetAdvertiseAddress !== undefined)
|
|
2265
|
+
return { center: cfg.configCenter, generation };
|
|
2266
|
+
const missing = [
|
|
2267
|
+
...(laneConfigured ? [] : [`the center lane (SEMA_REGISTRY_URL + ${CENTER_CREDENTIAL_ENV.token} or ${CENTER_CREDENTIAL_ENV.tokenFile} + SEMA_REGISTRY_WORKER)`]),
|
|
2268
|
+
...(cfg.fleetAdvertiseAddress === undefined ? ["the announce address (FLEET_ADVERTISE_ADDRESS)"] : []),
|
|
2269
|
+
];
|
|
2270
|
+
throw fleetEnvRefusal("member_unfenced", `fleet member cannot be fenced — env FLEET_GENERATION=${JSON.stringify(generation)} makes this engine a fleet member, ` +
|
|
2271
|
+
`but ${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not configured. A member renews its serving lease with the center and hears whether ` +
|
|
2272
|
+
`the center enforces the lease only in the heartbeat reply (the heartbeat needs the announce address); without them it would serve outside the fence. ` +
|
|
2273
|
+
`Configure them, or unset FLEET_GENERATION for a standalone engine.`);
|
|
2274
|
+
}
|
|
2275
|
+
function parseFleetLeaseOutageMaxMs(raw) {
|
|
2276
|
+
if (raw === undefined || raw === "")
|
|
2277
|
+
return FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS;
|
|
2278
|
+
const n = /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : Number.NaN;
|
|
2279
|
+
if (!Number.isSafeInteger(n) || n < SERVING_LEASE_TTL_FLOOR_MS) {
|
|
2280
|
+
const shown = raw.length > 40 ? `${raw.slice(0, 40)}…(${raw.length} chars)` : raw;
|
|
2281
|
+
throw new Error(`env FLEET_LEASE_OUTAGE_MAX_MS=${JSON.stringify(shown)} must be a whole number of milliseconds ≥ ${SERVING_LEASE_TTL_FLOOR_MS} (the serving-lease TTL floor; ` +
|
|
2282
|
+
`decimal digits only; unset = ${FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS}, 24 h). It bounds how long this engine keeps taking new submissions after its serving ` +
|
|
2283
|
+
`lease expired with the center unreachable — "0" and "infinite" are not values (the tolerance must be bounded and non-trivial). Refusing to boot.`);
|
|
2284
|
+
}
|
|
2285
|
+
return n;
|
|
2286
|
+
}
|
|
2205
2287
|
function parseFleetLeaseFailClosed(raw, requirePrincipal) {
|
|
2206
2288
|
const explicit = parseBoolWord("FLEET_LEASE_FAIL_CLOSED", raw);
|
|
2207
2289
|
const value = explicit ?? requirePrincipal;
|
|
2208
2290
|
registerKnob({ name: "FLEET_LEASE_FAIL_CLOSED", polarity: "posture", value, source: explicit !== undefined ? "env" : requirePrincipal ? "posture" : "default" });
|
|
2209
2291
|
return value;
|
|
2210
2292
|
}
|
|
2211
|
-
function
|
|
2293
|
+
function parseCenterBaseUrl(name, raw, role) {
|
|
2294
|
+
if (raw === undefined || raw === "")
|
|
2295
|
+
return undefined;
|
|
2296
|
+
const why = pathBaseUrlFlaw(raw);
|
|
2297
|
+
if (why !== undefined)
|
|
2298
|
+
throw new Error(`env ${name} ${why}; got ${JSON.stringify(buildDisplaySafeUrl(raw))}. ${role}`);
|
|
2299
|
+
return raw;
|
|
2300
|
+
}
|
|
2301
|
+
function parseConfigCenter(baseUrl, requirePrincipal, liveBaseUrl, webBaseUrl) {
|
|
2212
2302
|
const worker = process.env.SEMA_REGISTRY_WORKER || undefined;
|
|
2213
2303
|
const declared = readCenterCredentialDeclaration();
|
|
2214
2304
|
return {
|
|
2215
2305
|
baseUrl,
|
|
2306
|
+
...(liveBaseUrl !== undefined ? { liveBaseUrl } : {}),
|
|
2307
|
+
...(webBaseUrl !== undefined ? { webBaseUrl } : {}),
|
|
2216
2308
|
credential: createCenterCredentialSource(declared.from === "file" ? { ...declared, multiTenant: requirePrincipal } : declared, worker),
|
|
2217
2309
|
dryRun: boolEnv("SEMA_REGISTRY_DRY_RUN", false),
|
|
2218
2310
|
worker,
|
|
@@ -2336,7 +2428,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
2336
2428
|
"backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
|
|
2337
2429
|
"sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
|
|
2338
2430
|
"imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
|
|
2339
|
-
"agentConfigDir", "configLkgPath",
|
|
2431
|
+
"agentConfigDir", "configLkgPath", "configLkgEnabled",
|
|
2340
2432
|
];
|
|
2341
2433
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
2342
2434
|
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
@@ -2348,7 +2440,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
|
|
|
2348
2440
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
2349
2441
|
const INTEGRATIONS_GROUP_KEYS = [
|
|
2350
2442
|
"pluginsAllowHosts", "mcpServers", "a2aPeers", "a2aServe", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
2351
|
-
"skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "configProvider", "configLocalDir",
|
|
2443
|
+
"skillsDir", "centerCredentialRenewLeadMs", "configCenter", "fleetLeaseFailClosed", "fleetGeneration", "fleetScope", "fleetAdvertiseAddress", "fleetLeaseOutageMaxMs", "configProvider", "configLocalDir",
|
|
2352
2444
|
];
|
|
2353
2445
|
export const CONFIG_GROUP_KEYS = {
|
|
2354
2446
|
store: STORE_GROUP_KEYS,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 排空的**一只入口**(编排器长远稿片 2 件 2;契约型 = settings-schema 8.1.0 `DrainRequest` / `DrainReceipt` / `EngineDraining`)。
|
|
3
|
+
*
|
|
4
|
+
* ## 规则
|
|
5
|
+
*
|
|
6
|
+
* 1. **一只入口,两条车道**:进入排空只有 `boot/shutdown.ts` 的 `enterDrain`(装在 {@link DrainState.enter} 上)一处。Docker 车道 / 中心直调
|
|
7
|
+
* 经 `POST /v1/admin/drain`(体 = `DrainRequest`);Kubernetes 车道经服务租约被拒(`fleet/serving-lease.ts`,`actionId = lease:<代际>`);
|
|
8
|
+
* 非编排路径 = 停止信号(SIGTERM)/ SIGHUP 升级 / 附着租约自退,动作号由引擎自铸({@link ENGINE_DRAIN_CAUSES})。全部汇到同一只入口。
|
|
9
|
+
* 2. **期限在进入那一拍定死**:请求带的 `deadlineAt` 原样;引擎自起的排空 = 起点 + 此刻的排空窗(`drainGraceMs`)。之后改窗(中心热改
|
|
10
|
+
* `limits.drainGraceMs`)不动在跑的期限 —— 排空驱动读的是期限,不是窗。
|
|
11
|
+
* 3. **幂等**:已在排空 ⇒ 不重置,回执 `already_draining` 带**第一次**的动作 / 起点 / 期限(不论这次的动作号是否相同)。
|
|
12
|
+
* 4. **身份**:逐字段比,任一不符 ⇒ 409 `conflict.fleet_identity`,零状态变化。
|
|
13
|
+
* · `scope`:两侧都经 {@link fleetScopeOf} 读成值(缺席 = settings-schema 的缺省 scope,线上任何一处缺席的 scope 都读成它)再严格相等。
|
|
14
|
+
* 引擎这一侧的唯一来源 = env `FLEET_SCOPE`(`config.fleetScope`,`config.ts` 解析一次);请求不带 `scope` = 点名缺省 scope。
|
|
15
|
+
* · `worker` / `expectedGeneration`:缺席对缺席(非 fleet 成员没有代际,不带 `expectedGeneration` 的请求是单机形,不是通配)。
|
|
16
|
+
* · `instanceId`:请求带了才比(它是引擎每进程自铸的身份,`/health` 上可读);「点名的是不是别的进程」与 run 行同一只判据
|
|
17
|
+
* `runProvenElsewhere`。
|
|
18
|
+
* 引擎自己的 worker / instanceId 读不出 ⇒ 带了这一项的请求不符(认不出自己就不认任何点名;没配 `SEMA_REGISTRY_WORKER` 的引擎因此
|
|
19
|
+
* 只能用停止信号排空)。
|
|
20
|
+
* 5. **进入那一拍让出在飞**:本副本可抢占的在飞腿经 core `TaskSpec.preemptSignal` 让出(在下一个干净的回合边界耐久停驻,
|
|
21
|
+
* `resource_limit` / `preempt`;不合格的腿上 core 静默不动,等到期限硬停 —— 既有 `crashed` 臂);活的审批卡按断连同路改路耐久停驻
|
|
22
|
+
* (协调器 `beginDrain`,core `onAsk` 答 `"unavailable"` 的合同);排空期间新登记的可抢占腿在登记那一拍让出(同一条规则)。
|
|
23
|
+
*
|
|
24
|
+
* 纯类型 + 纯函数叶(值 import 只有 scope 读法叶与 run 行判词叶)。
|
|
25
|
+
*/
|
|
26
|
+
import type { DrainReceipt, DrainRequest, EngineDraining } from "@sema-agent/settings-schema/fleet";
|
|
27
|
+
/**
|
|
28
|
+
* 引擎自铸的动作号 —— **一张表**(排空事实与回执里不来自请求体的全部拼法;请求体的 `actionId` 由调用方自取):
|
|
29
|
+
* · {@link ENGINE_DRAIN_CAUSES}:没有请求承载的排空成因(停止信号 / SIGHUP 升级 / 附着租约自退);
|
|
30
|
+
* · {@link HARD_STOP_ACTION_ID}:硬停(未经排空)已在进行时排空口回执里的动作号 —— 不是一种排空成因,只说「这只进程已经在停,而且比你要的更快」;
|
|
31
|
+
* · `lease:<代际>`:服务租约被拒的排空({@link leaseDrainActionId},每代一只:同一代被拒两次是同一个动作)。
|
|
32
|
+
* 按成因穷举的地方({@link DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL})加一种成因不补格 = 编译红。
|
|
33
|
+
*/
|
|
34
|
+
export declare const ENGINE_DRAIN_CAUSES: readonly ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
|
|
35
|
+
export type EngineDrainCause = (typeof ENGINE_DRAIN_CAUSES)[number];
|
|
36
|
+
export declare const HARD_STOP_ACTION_ID = "hard-stop";
|
|
37
|
+
export type LeaseDrainActionId = `lease:${string}`;
|
|
38
|
+
/** 服务租约被拒时的动作号(每代一只)。 */
|
|
39
|
+
export declare function leaseDrainActionId(generation: string): LeaseDrainActionId;
|
|
40
|
+
/**
|
|
41
|
+
* 每种排空成因的**入口那一拍算不算本次排空的第一发停止信号**(`boot/shutdown.ts` 的「第二发 = 硬停」读它;它答的不是「这是不是 `StopSignal`」——
|
|
42
|
+
* `shutdown.ts` 的 `StopSignal` 闭集只有 SIGTERM / SIGINT,那是「哪些信号走停止处理器」)。算 ⇒ 之后隔 ≥ 最小间隔的一发即硬停;不算 ⇒ 期限由入口定死,
|
|
43
|
+
* 之后到达的第一发停止信号只被吸收(不缩短期限)。SIGHUP 算:SIGHUP 升级成排空要的就是**第二发** SIGHUP(第一发只开空闲窗),那是人 / 宿主又发了一次
|
|
44
|
+
* 停的意图,与 SIGTERM 进的排空同一个读法(7.120.0 起的行为)。闭集 Record。
|
|
45
|
+
*/
|
|
46
|
+
export declare const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL: {
|
|
47
|
+
readonly [C in EngineDrainCause]: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** 入口的入参:动作号 + 期限(epoch ms)。 */
|
|
50
|
+
export interface DrainEntry {
|
|
51
|
+
readonly actionId: string;
|
|
52
|
+
readonly deadlineAt: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
|
|
56
|
+
* · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
|
|
57
|
+
* · `enter` —— 唯一入口(收尾链装;缺席 = 这个进程没有收尾链,如嵌入宿主 / 测试夹具);
|
|
58
|
+
* · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
|
|
59
|
+
* · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。
|
|
60
|
+
*/
|
|
61
|
+
export interface DrainState {
|
|
62
|
+
draining: boolean;
|
|
63
|
+
since?: number;
|
|
64
|
+
actionId?: string;
|
|
65
|
+
deadlineAt?: number;
|
|
66
|
+
enter?: (entry: DrainEntry) => DrainReceipt;
|
|
67
|
+
yieldInflight?: () => void;
|
|
68
|
+
inflight?: () => number;
|
|
69
|
+
lastActivityAt?: () => number;
|
|
70
|
+
attachedStreams?: () => number;
|
|
71
|
+
lastAttachEndedAt?: () => number;
|
|
72
|
+
}
|
|
73
|
+
/** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
|
|
74
|
+
export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
|
|
75
|
+
/** 本进程的身份。`scope` 已读成值(`config.fleetScope`);其余三项与 `/health` 上读到的同一份。 */
|
|
76
|
+
export interface EngineIdentity {
|
|
77
|
+
readonly scope: string;
|
|
78
|
+
readonly worker?: string;
|
|
79
|
+
readonly generation?: string;
|
|
80
|
+
readonly instanceId?: string;
|
|
81
|
+
}
|
|
82
|
+
/** 身份四元的字段名(请求侧拼法;409 拒句按它点名,不回显值)。 */
|
|
83
|
+
export declare const DRAIN_IDENTITY_FIELDS: readonly ["scope", "worker", "expectedGeneration", "instanceId"];
|
|
84
|
+
export type DrainIdentityField = (typeof DRAIN_IDENTITY_FIELDS)[number];
|
|
85
|
+
/** 规则 4:不符的字段(按 {@link DRAIN_IDENTITY_FIELDS} 序;空 ⇔ 认得)。 */
|
|
86
|
+
export declare function drainIdentityMismatches(req: Pick<DrainRequest, "scope" | "worker" | "expectedGeneration" | "instanceId">, self: EngineIdentity): DrainIdentityField[];
|
|
87
|
+
//# sourceMappingURL=drain-entry.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fleetScopeOf } from "../config-types.js";
|
|
2
|
+
import { runProvenElsewhere } from "../plugins/store-contracts.js";
|
|
3
|
+
export const ENGINE_DRAIN_CAUSES = ["signal:SIGTERM", "signal:SIGHUP", "self-exit:attachment"];
|
|
4
|
+
export const HARD_STOP_ACTION_ID = "hard-stop";
|
|
5
|
+
export function leaseDrainActionId(generation) {
|
|
6
|
+
return `lease:${generation}`;
|
|
7
|
+
}
|
|
8
|
+
export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
|
|
9
|
+
"signal:SIGTERM": true,
|
|
10
|
+
"signal:SIGHUP": true,
|
|
11
|
+
"self-exit:attachment": false,
|
|
12
|
+
};
|
|
13
|
+
export function drainingFactOf(s) {
|
|
14
|
+
return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
|
|
15
|
+
}
|
|
16
|
+
export const DRAIN_IDENTITY_FIELDS = ["scope", "worker", "expectedGeneration", "instanceId"];
|
|
17
|
+
export function drainIdentityMismatches(req, self) {
|
|
18
|
+
return DRAIN_IDENTITY_FIELDS.filter((f) => {
|
|
19
|
+
switch (f) {
|
|
20
|
+
case "scope":
|
|
21
|
+
return fleetScopeOf(req.scope) !== self.scope;
|
|
22
|
+
case "worker":
|
|
23
|
+
return self.worker === undefined || req.worker !== self.worker;
|
|
24
|
+
case "expectedGeneration":
|
|
25
|
+
return req.expectedGeneration !== self.generation;
|
|
26
|
+
case "instanceId":
|
|
27
|
+
return req.instanceId !== undefined && (self.instanceId === undefined || runProvenElsewhere({ instanceId: req.instanceId }, self.instanceId));
|
|
28
|
+
default: {
|
|
29
|
+
const unhandled = f;
|
|
30
|
+
throw new Error(`drain identity: unknown field ${String(unhandled)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=drain-entry.js.map
|
|
@@ -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
|