@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.
Files changed (86) hide show
  1. package/MIGRATION.md +75 -0
  2. package/USAGE.md +67 -20
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +33 -9
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/resolve-spec.js +1 -0
  10. package/dist/boot/shutdown.d.ts +9 -10
  11. package/dist/boot/shutdown.js +54 -16
  12. package/dist/boot/stage-09-leader.d.ts +2 -9
  13. package/dist/boot/stage-10-http-server.d.ts +1 -9
  14. package/dist/boot/stage-10-http-server.js +31 -8
  15. package/dist/capabilities/composite-members.d.ts +1 -1
  16. package/dist/capabilities/composite-members.js +1 -1
  17. package/dist/config-catalog.js +11 -5
  18. package/dist/config-center/center-request.d.ts +16 -1
  19. package/dist/config-center/center-request.js +36 -5
  20. package/dist/config-types.d.ts +39 -3
  21. package/dist/config-types.js +6 -0
  22. package/dist/config.d.ts +33 -4
  23. package/dist/config.js +99 -7
  24. package/dist/fleet/drain-entry.d.ts +87 -0
  25. package/dist/fleet/drain-entry.js +35 -0
  26. package/dist/fleet/engine-signal.d.ts +144 -0
  27. package/dist/fleet/engine-signal.js +60 -0
  28. package/dist/fleet/serving-lease.d.ts +47 -0
  29. package/dist/fleet/serving-lease.js +235 -0
  30. package/dist/fleet-client.d.ts +35 -19
  31. package/dist/fleet-client.js +49 -25
  32. package/dist/http/approval-sweeps.js +3 -5
  33. package/dist/http/dispatch.js +18 -13
  34. package/dist/http/health-self-report.d.ts +60 -0
  35. package/dist/http/health-self-report.js +53 -0
  36. package/dist/http/principal-gate.d.ts +8 -1
  37. package/dist/http/principal-gate.js +1 -0
  38. package/dist/http/readiness.d.ts +25 -5
  39. package/dist/http/readiness.js +31 -6
  40. package/dist/http/resume-legs.js +1 -1
  41. package/dist/http/route-ctx.d.ts +6 -4
  42. package/dist/http/route-table.d.ts +3 -0
  43. package/dist/http/route-table.js +4 -1
  44. package/dist/http/routes/a2a-serve.d.ts +1 -1
  45. package/dist/http/routes/a2a-serve.js +5 -5
  46. package/dist/http/routes/admin-drain.d.ts +12 -31
  47. package/dist/http/routes/admin-drain.js +15 -9
  48. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +3 -3
  50. package/dist/http/routes/capabilities.js +2 -1
  51. package/dist/http/routes/devices.js +3 -2
  52. package/dist/http/routes/leader.d.ts +1 -1
  53. package/dist/http/routes/leader.js +1 -1
  54. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  55. package/dist/http/routes/memory-consolidation.js +1 -1
  56. package/dist/http/routes/notify-wake.d.ts +1 -1
  57. package/dist/http/routes/notify-wake.js +1 -1
  58. package/dist/http/routes/rules.js +4 -3
  59. package/dist/http/routes/runs.d.ts +2 -2
  60. package/dist/http/routes/runs.js +2 -2
  61. package/dist/http/routes/side-query.d.ts +1 -1
  62. package/dist/http/routes/side-query.js +1 -1
  63. package/dist/http/routes/tasks.d.ts +2 -2
  64. package/dist/http/routes/tasks.js +3 -3
  65. package/dist/http/server.d.ts +7 -21
  66. package/dist/http/server.js +20 -1
  67. package/dist/http/wire-types.d.ts +7 -1
  68. package/dist/model-reach-scope.d.ts +15 -3
  69. package/dist/model-reach-scope.js +3 -2
  70. package/dist/model-unreachable-notice.d.ts +2 -1
  71. package/dist/model-unreachable-notice.js +1 -0
  72. package/dist/observability/fail-open.d.ts +16 -0
  73. package/dist/observability/fail-open.js +16 -0
  74. package/dist/plugins/one-time-migrations.js +1 -1
  75. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  76. package/dist/plugins/remote-env-device.js +0 -1
  77. package/dist/request-key-closure.js +1 -1
  78. package/dist/rules-consent.d.ts +6 -2
  79. package/dist/rules-consent.js +3 -0
  80. package/dist/runs.js +1 -1
  81. package/dist/tool-approval.d.ts +7 -0
  82. package/dist/tool-approval.js +4 -1
  83. package/dist/trace/core-keyset-guard.d.ts +2 -2
  84. package/dist/trace/self-minted-wire.d.ts +64 -0
  85. package/dist/trace/self-minted-wire.js +30 -0
  86. package/package.json +2 -2
package/MIGRATION.md CHANGED
@@ -7,6 +7,81 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.121.0 —— 编排器片 2(引擎侧):`/readyz` 唯一探针 · 排空口契约体 · 服务租约客户端 · fleet 成员必须能被围栏
11
+
12
+ **升级前核查(一句话)**:探针改指 `/readyz`(第 1 条);调 `POST /v1/admin/drain {reason}` 的改发契约体,多租引擎的排空请求带它的 `scope`(第 2 条);读 `/health.drainReason` / `modelPlaneDeferred` 的删读点(第 2 / 5 条,两键都已删);
13
+ 设了 `FLEET_GENERATION` 的部署核中心三件与 `FLEET_ADVERTISE_ADDRESS` 都在、渲染器为多租引擎渲 `FLEET_SCOPE`(第 4 条);中心在真能答续租端点之前不宣告 `servingLease`(第 7 条);
14
+ 三只中心基址不带凭据 / query / fragment、`FLEET_ADVERTISE_ADDRESS` 设了就是 http(s)(第 7 条,没接中心也拒启)。
15
+
16
+ 1. **探针路径只有 `/readyz`**:镜像 `HEALTHCHECK` 与 sema-up 的 chart / cluster 清单 readinessProbe 改指 `/readyz`(排空中、roster 未落、服务租约被拒 ⇒ 503);
17
+ `/health` 只做诊断,恒 200。自己写探针的部署把 readinessProbe / LB 健康检查改成 `/readyz`(该口 7.102.0 起就在);要保留存活探针的用 TCP 探端口,
18
+ **不要**拿 `/readyz` 当「重启我」的信号(中心宕机时整队副本同卡 roster 未落,会一起重启)。仍探 `/health` 的老执行器 / 老编排器:把底线抬到能读 `/readyz` 的版本(≥7.102.0 的引擎都答它)之后再退役 `/health` 探针。
19
+ 2. **排空口 `POST /v1/admin/drain` 的体换成契约形**(settings-schema `DrainRequest`):`{ actionId, worker, deadlineAt, scope?, expectedGeneration?, instanceId?, term? }`,
20
+ 回执 `{ actionId, since, deadlineAt, state: "draining" | "already_draining" }`。**它现在真的进入排空**(修前只记一句停机因由,排空靠随后的 SIGTERM)。
21
+ 身份逐字段比,不符 ⇒ 409 `conflict.fleet_identity`,什么都不变:`scope` 两侧都读成值再比 —— 引擎这一侧 = env `FLEET_SCOPE`(第 4 条),请求不带 `scope`
22
+ 与引擎没设 `FLEET_SCOPE` 都读成缺省 scope `global`(所以单租部署不带 `scope` 照常通;多租引擎的请求要带它自己的 scope);`worker` / `expectedGeneration`
23
+ 缺席对缺席(非 fleet 成员不带 `expectedGeneration`);`instanceId` 在场才比(从本机 `GET /health` 读)。同一动作号重发 ⇒ 200 `already_draining`,回显第一次的
24
+ `since` / `deadlineAt`。人手排空:`actionId` 自取(如 `manual:<uuid>`)。没配 `SEMA_REGISTRY_WORKER` 的引擎没有 worker 身份,排空口对它恒 409 —— 用 SIGTERM。
25
+ **排空之后的停止信号**:「第二发停止信号 = 立即硬停」锚的是**本次排空的第一发停止信号**。SIGTERM 进的排空,那一发就是第一发,再隔
26
+ ≥ `DRAIN_SECOND_SIGNAL_MIN_GAP_MS`(缺省 250 ms)的一发硬停(与 7.120.0 同)。经排空口(或服务租约被拒)进的排空,之后到达的**第一发**停止信号被吸收:
27
+ 期限仍是请求定死的那一个,一行 info `drain_signal_after_request_drain_ignored`;再隔 ≥ 250 ms 的一发才硬停。k8s 例:`preStop` 调排空口(200)→ kubelet 发一发
28
+ SIGTERM(吸收,在飞照常跑到期限)→ `terminationGracePeriodSeconds` 到点 SIGKILL —— 把 `terminationGracePeriodSeconds` 设得不短于排空请求的期限。
29
+ **退役**:`{reason}` 声明形(400 `request.field_invalid`)、`/health.drainReason`、503 draining 体的 `reason` 键。读这两键的消费方删读点。
30
+ 3. **排空期间放行的请求**:取消 / 停止、HITL 应答(含耐久 `/decide`、PARKED 停驻卡赎回、计划审阅)、续跑(助手 resume、子代 resume)、读面、重挂;
31
+ 其余计费提交照旧 503 `draining`。**变化**:HITL 应答与续跑修前也吃排空 503,现在放行 —— 它们在排空的引擎上开出的腿于下一个干净回合边界让出
32
+ (可抢占的部署:`RESOURCE_SUSPEND` 打开 ∧ 耐久店 ∧ 远端执行环境,腿耐久停驻、可在替补上续;不可抢占的部署:腿跑到期限,到期硬停走既有的崩溃臂)。
33
+ 进入排空那一拍,本副本可抢占的在飞腿让出、活的审批卡改路耐久停驻(修前到期限硬停那一拍才收场)。🔴 开了 `RESOURCE_SUSPEND` 的部署:每一次排空
34
+ (含 SIGTERM 滚动重启)里在飞的可抢占 run 不再跑完,而是耐久停驻(`resource_limit` / `preempt`),要调 `POST /v1/assistant/tasks/:id/resume` 续 ——
35
+ 没有调度方替你续的部署,升级后把这一步加进滚动重启的收尾(列 `GET /v1/assistant/tasks` 里停驻的,逐条续)。没有停驻设施(无 checkpoint 店)的部署上,进入排空那一拍
36
+ 未决的审批卡、以及之后新出的卡按**拒**处置(与断连同一条路:无处停驻就无头自动拒,fail-closed)。排空期限在进入那一拍定死:中心热改 `limits.drainGraceMs`
37
+ 不再改在跑的排空,只影响**下一次**由停止信号(或附着租约自退)起的排空的窗。
38
+ 4. **fleet 成员与服务租约**(新旋钮):`FLEET_GENERATION`(配置渲染器写;规范十进制串 `"0"` / `"7"`,空串 / 前导零 / 符号 / 空白拒启;缺席 = 非 fleet 成员)、
39
+ `FLEET_LEASE_OUTAGE_MAX_MS`(服务租约中心不可达的容忍上限,缺省 86400000 = 24 h,< 60000 / 0 / 非整数拒启)。🔴 **设了 `FLEET_GENERATION` 就必须能被围栏**:
40
+ 中心三件(`SEMA_REGISTRY_URL` + 凭证 + `SEMA_REGISTRY_WORKER`)与 `FLEET_ADVERTISE_ADDRESS` 缺一 ⇒ 监听之前拒启 `[config.fleet_member_unfenced]`
41
+ (能力位只在心跳答复里,不公告就听不到;判在 `loadConfig`,早于任何装配)。不打算受围栏的单机引擎不设 `FLEET_GENERATION`。
42
+ 🔴 **`FLEET_SCOPE`(新旋钮)**:引擎所在的租户 scope —— 配置渲染器 / 运维请为**多租引擎**渲它(值 = 这只引擎的 per-worker 令牌绑的那个 scope);
43
+ 缺席 = 缺省 scope `global`,与 `FLEET_GENERATION` 不联动(成员不设也不拒启)。在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启)。
44
+ 它只进排空口的身份比对(第 2 条);续租体与心跳**不带**它(那两处的 scope 由中心凭证绑定)。settings-schema 8.3.0 起它是保留 worker env 名(`FLEET_SCOPE_ENV`)。
45
+ **`FLEET_ADVERTISE_ADDRESS` 改由 `loadConfig` 无条件解析一次**:设了就必须是 http(s) URL、不带 userinfo / query / fragment(与中心基址三席同一条判据,
46
+ 第 7 条),没接中心也拒启(修前只在中心车道配齐时判、只判 http(s));空串 = 未设。
47
+ fleet 面拒启句的码挂在异常 `code` 上(fatal 行印成 `Error [config.fleet_…]: …`):`config.fleet_generation_invalid` / `config.fleet_scope_invalid` /
48
+ `config.fleet_advertise_address_invalid` / `config.fleet_member_unfenced`。
49
+ fleet 成员的心跳与 `/health` 多三键 `generation` / `leaseState`(`none` / `granted` / `expired` / `denied`)/ `leaseUntil`(只在 `granted`);心跳体就是契约
50
+ `InstanceAnnouncement` 的形(settings-schema ≥ 8.2.0 才类型化这三键 —— 更老的契约包按 strip 形读,三键读不到也不拒)。租约只在中心于心跳答复里宣告
51
+ `capabilities.servingLease: true` 之后生效;没宣告 ⇒ `leaseState: "none"`、零续租、零 503(与 7.120.0 同)。只有解析成 JSON 对象的答复体才算一次能力读数:
52
+ 答复体读不出 / 不是对象(空体、截断、`null`、数组)⇒ 没有读数,上一次的位与租约钟原样,一段连拍一行 warn `fleet_announce_reply_unreadable`。
53
+ 中心没确认续租(不可达 / `unknown`)与能力读数里没宣告 `servingLease` 两条放行逐次进 fail-open 计数(`server.fleet.serving-lease-unconfirmed` /
54
+ `server.fleet.serving-lease-capability-absent`)。续租打 `POST /api/fleet/live/serving-lease`(契约
55
+ `FLEET_PATHS.servingLease`)。中心以正面证据拒(被取代 / 已退役)⇒ 排空、到期自停,并在数据根记一只 `fleet-serving-lease-denied.json` —— 同 worker 同代际
56
+ 重启后起步即排空,第一条心跳就是排空态;要让这只实例重新服务,由中心换代(渲染器写新 `FLEET_GENERATION`),不要手删那只文件。那只文件**写不进**(数据根只读 /
57
+ 盘满)⇒ 本进程照拒照停,但重启后不记得、在第一次续租答复之前会接单 —— 响亮:error 行 `fleet_serving_lease_denial_not_persisted` + fail-open 计数
58
+ `server.fleet.denied-marker-unpersisted`;数据根要可写。中心不可达 / 答 `unknown` ⇒ 不排空(每段连败一行运维 warn `fleet_serving_lease_unconfirmed`);租约过期满 `FLEET_LEASE_OUTAGE_MAX_MS` ⇒ 新工作 503
59
+ `state.lease_expired`(`Retry-After: 60`;放行同第 3 条,`/readyz` 不翻)。
60
+ 5. **扣着的候选改由 `restart.withheld: true` 上心跳与 `/health`**(扣着而有被扣的重启片时;合成:已发布的重启信号恒胜,只差发布重试的不铸);
61
+ `/health.restartRequired` 只在可执行时在场(被扣的不算,读数与 7.120.0 同)。🔴 **`/health.modelPlaneDeferred` 本版删除**(零别名),它的两半各归一张脸:
62
+ 「要重启但扣着」读 `restart.withheld`;「应用着的配置没有耐久接力」(原 `noHandoff`)读新键 **`/health.configHandoff = { state: "missing", version, since }`**
63
+ (只在 `/health`;在场 ⇔ 部署声明了耐久接力 —— `CONFIG_LKG_DURABLE=true` 且 LKG 启用 —— 而应用着的那一代没在下一次起服将用的凭证身份的 LKG 桶里确认耐久落盘,
64
+ 含部分应用的那一代与凭据换了人的情形;
65
+ 没有耐久声明的部署恒缺席:那里重启靠中心重拉是部署形本身,7.120.x 的 `modelPlaneDeferred` 在这种部署上同样只在保留候选时出现)。迁移读法:原来 `modelPlaneDeferred` 在场 ⇒ 现在 `configHandoff` 在场;原来带 `blockedReasons` ⇒ 现在 `restart.withheld: true`
66
+ 且 `restart.reasons` = 那几片;只差发布重试那一形由 `configHandoff` 承载,`restart.withheld` 不在场。
67
+ 6. **能力位**:`GET /v1/capabilities` 的 `fleet` 对象 +`leaseClient: true` / `generationEcho: true`(随 `fleet` 对象在场)。中心据此知道引擎带服务租约客户端与代际回声。
68
+ 引擎这一位刻意不叫 `servingLease`:那是中心在心跳答复里宣告「我答续租」的位,两边同名会把「客户端在」读成「租约在执行」。
69
+ 7. **活性基址 `SEMA_REGISTRY_LIVE_URL`(新旋钮,可选)与版本矩阵**:心跳 / 注销与续租(settings-schema `FLEET_LIVE_PATHS`)打它,缺席 = `SEMA_REGISTRY_URL`;
70
+ 用量、配额租约、拉配置始终打 `SEMA_REGISTRY_URL`。🔴 中心基址三席(`SEMA_REGISTRY_URL` / `SEMA_REGISTRY_LIVE_URL` / `SEMA_REGISTRY_WEB_URL`)同一条判据:
71
+ 可解析的 http(s) URL,**不带**凭据 / query / fragment(基址后面要拼路径;`http://x?a=1` 拼出来是 `http://x?a=1/api/…`),坏形拒启,没接中心也拒 ——
72
+ 修前 `SEMA_REGISTRY_URL` 起服不判、活性基址只判 http(s)。请求期对同一份基址再判一次(自拼 `ServiceConfig` 的嵌入方同规)。版本矩阵:引擎 ≥ 7.121.0 带续租客户端;
73
+ 中心宣告 `servingLease` **必须**与它真能答 `/api/fleet/live/serving-lease` 同版 —— 端点还没有的中心只宣告了位,引擎每次续租得 404,按不可达处置:
74
+ 不排空,但租约过期满 `FLEET_LEASE_OUTAGE_MAX_MS` 后新工作 503 `state.lease_expired`。中心停止宣告(位缺席)⇒ 下一条心跳起回到 `none`,503 随之解除。
75
+ 8. **7.120.0 余项的三处文本与一处嵌入方类型席**:① `POST /v1/rules/cc-import/prepare` 某层 `root` 不是绝对路径时,`400 request.body_shape` 的文本从 scope 那一句换成
76
+ `layers[<i>].root must be an absolute path`(`<i>` 从 0 起,第一个被拒的层;码与状态不变);② `POST /v1/devices/enroll-tokens` 与 `POST /v1/devices/enroll` 对不带租户 scope 的身份
77
+ (含运维主体)的 `403 auth.forbidden` 文本改说「入籍要带租户 scope 的注册中心登录身份,运维主体不带 scope、不能入籍」,不再指向配置 `OPERATOR_PRINCIPALS`(设备管理的其余口原句不变);
78
+ ③ 一次性迁移对没有活 approval 的坏 scope 规则行(`kind: "revoked-rule"`)的逐行 warn 文本改成不依赖墓碑与态的一句。三处都只动文本 —— 判错误请用 `errorCode` 与状态码,不要整句匹配。
79
+ ④ **嵌入方**(自己拼 `ServiceConfig` 的宿主):新增必填席 `configLkgEnabled: boolean`(`CONFIG_LKG_ENABLED` 的值,缺省开;远程配置的 LKG 盘缓存开关),由 `loadConfig` 一次铸出;
80
+ config-center 的两个读点只读这一席,起服之后改进程环境变量不再影响它。内部的读函数 `configLkgEnabled()` 已删(它从未在包根导出里,包的 `exports` 也不开放深路径);
81
+ 自己拼配置的宿主在配置里补这一席,照原先环境变量的取值给 `true` / `false`。同版另两只必填席:`fleetLeaseOutageMaxMs: number`(缺省 86 400 000)、
82
+ `fleetScope: string`(缺省 `"global"`);可选席 `fleetAdvertiseAddress?: string`(公告环与围栏判官读它,不再读进程环境变量)。`ServiceDeps.drainState` 的型换成
83
+ `DrainState`(排空事实四键 `draining` / `since` / `actionId` / `deadlineAt` + 入口 `enter`;没有 `reason`),`ServiceDeps.planeDeferredState` 席已删。
84
+
10
85
  ## 7.120.0 —— 引擎 7.46.0:子代工具清单回到开跑前拒退役拼法、管到内建工具 · 配置根搬家后同护 · 回显条目一种渲染 · 路径规则锚在装入处
11
86
 
12
87
  **升级前核查(一句话)**:设了 `SEMA_CONFIG_DIR` 的部署确认那个目录能建出来、不是空串;`CONFIG_LKG_PATH` 不是空串;
package/USAGE.md CHANGED
@@ -75,23 +75,23 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
75
75
 
76
76
  > 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
77
77
 
78
- **探针:存活看 `/health`,就绪看 `/readyz`(7.102.0+)**。`/health` 恒 200(liveness:进程在答);只认状态码的**摘流**
79
- 探针(k8s `readinessProbe` / Traefik / 负载均衡健康检查)读不到它体里的 `ready:false`,要摘流就指 `GET /readyz`:就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`。
80
- `reasons` 闭集两词:`draining`(SIGTERM 后优雅排空中)、`model_roster_pending`(`CONFIG_REQUIRE_ROSTER=true`
81
- 且中心模型名册尚未落地)。两者都免鉴权(与 `/health` 同位,探针带不了 token)。🔴 **库连不上不算未就绪**:
82
- `storeLive:false` 只在 `/health` 体里披露 —— 一次库抖若进就绪门,整队副本会在同一拍被摘光(共模失效)。
83
- ⚠️ **重启语义的探针别指 `/readyz`**(镜像 `HEALTHCHECK` 配 Swarm / autoheal、k8s `livenessProbe`):中心宕机时整队副本
84
- 同卡 `model_roster_pending`,指 `/readyz` 就会一起被重启、再一起卡住 —— 同一种共模失效。它们留在 `/health`(本仓
85
- `Dockerfile` 的 `HEALTHCHECK` 就是 `/health`)。
78
+ **探针路径只有 `/readyz`(7.121.0 起;该口 7.102.0+ 就在)**。`/health` 是诊断体、恒 200,不当探针。只认状态码的**摘流**探针
79
+ (k8s `readinessProbe` / Traefik / 负载均衡健康检查 / 镜像 `HEALTHCHECK`)指 `GET /readyz`:就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`。
80
+ `reasons` 闭集两词:`draining`(排空中 —— SIGTERM、排空口、服务租约被拒都进这一态)、`model_roster_pending`(`CONFIG_REQUIRE_ROSTER=true`
81
+ 且中心模型名册尚未落地)。两者都免鉴权(探针带不了 token)。🔴 **库连不上不算未就绪**:
82
+ `storeLive:false` 只在 `/health` 体里披露 —— 一次库抖若进就绪门,整队副本会在同一拍被摘光(共模失效)。服务租约**过期**(中心不可达)同样不翻 `/readyz`。
83
+ ⚠️ **`/readyz` 的 503 是「别往我这儿路由」,不是「重启我」**:中心宕机时整队副本同卡 `model_roster_pending`,拿它当重启信号就会一起被重启、
84
+ 再一起卡住。本仓 `Dockerfile` 的 `HEALTHCHECK` 指 `/readyz`(Docker 的 unhealthy 只影响路由 / 依赖判定);执行器不要按它重启容器。
85
+ 要存活探针的部署用 TCP 探端口。
86
86
  ```yaml
87
- # k8s:liveness 打 /health(进程死才重启),readiness 打 /readyz(不就绪只摘流,不重启)
88
- livenessProbe:
89
- httpGet: { path: /health, port: 8090 }
90
- periodSeconds: 10
87
+ # k8s:readiness 打 /readyz(不就绪只摘流,不重启);要 liveness 就探端口,不打 HTTP
91
88
  readinessProbe:
92
89
  httpGet: { path: /readyz, port: 8090 }
93
90
  periodSeconds: 5
94
91
  failureThreshold: 1 # 排空 / 名册未落要立刻摘,不等三拍
92
+ livenessProbe:
93
+ tcpSocket: { port: 8090 }
94
+ periodSeconds: 10
95
95
  ```
96
96
 
97
97
  **可选 — 多网关 failover + Anthropic 路由(消除单网关 SPOF)**
@@ -488,6 +488,20 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
488
488
  **7.106.0+**(S-810):候选**部分应用**(某组被整组拒、其余组已提交)的拍也照同一律算这一信号 —— 修前那一拍不签;被拒的是审批组 / 模型面组时
489
489
  (新进程 boot 重判可能拒启)信号扣下、只出 warn `restart_signal_withheld_boot_may_refuse`(点名组与差异)。
490
490
  钉:`test/health-readyz.test.ts` / `test/health-readiness-single-source.test.ts`。
491
+ - **配置接力位 `configHandoff`(7.121.0+,只在 `/health`,不上心跳)**:`{ state: "missing", version, since }`,**在场 ⇔ 部署声明了耐久接力(`CONFIG_LKG_DURABLE=true`
492
+ ∧ LKG 启用)∧ 应用着的那一代没在下一次起服将用的凭证身份的 LKG 桶里确认耐久落盘** —— 中心断着时重启按**此刻的凭证身份**选 LKG 桶,那只桶里没有这一代就回到
493
+ 别的配置。三种形:LKG 发布失败(保留候选、启动期技能债)、部分应用(某组被整拒、其余组已热应用,LKG 停在上一个完整世代)、凭据换了人(凭证文件换成别的身份,
494
+ 新身份的桶里还没有这一代)。在那只身份的桶里确认即缺席。**没有耐久声明的部署恒缺席**:那里「重启靠中心重拉」是部署形本身(配置时声明的事实),不是事故;没接中心 / 本地腿
495
+ 也缺席。`version` = 应用着的那一代,`since` = 这一代开始没有确认接力的时刻。与 `restart.withheld`(要重启但扣着)是两个事实两张脸,可同在。7.120.x 的 `modelPlaneDeferred` 已删。
496
+ - **自报位(7.121.0+)**:`/health` 恒带三键 `auth` / `operator` / `center`(缺席 = 老引擎),给壳在**带凭据之前**判出路用,
497
+ 全部是装配事实的投影、不是另立的规则:`auth = { required, kinds, identityAttach }` —— `required` = 全局凭据门武装(配了
498
+ `SERVICE_AUTH_TOKEN(S)`),`kinds` ⊆ `service_token` / `sso`(装了鉴权桥:登记中心的登录令牌)/ `direct_door_jwt`(直连门),
499
+ `identityAttach` = `available`(kinds 里有 `sso` 或 `direct_door_jwt`)/ `unavailable`;`operator` = `present`(`OPERATOR_PRINCIPALS`
500
+ 非空)/ `absent`;`center = { issuer?, webBaseUrl?, sessionsVisible }` —— `issuer` = 鉴权桥的 issuer(`AUTH_BRIDGE_ISSUER`,缺席回退
501
+ `SEMA_REGISTRY_URL`;没有桥 ⇒ 缺席),`webBaseUrl` = `SEMA_REGISTRY_WEB_URL` 原样(缺席即缺席),`sessionsVisible` = `true` /
502
+ `false` / `"unknown"`:中心车道配齐 ∧ `FLEET_ADVERTISE_ADDRESS` 在 ∧ 鉴权桥在 ∧ 会话枚举面在,缺一 ⇒ `false`;齐了而多租户 ⇒ `true`,
503
+ 单用户 ⇒ `"unknown"`(一条会话归谁看提交它的请求带没带身份)。两只 URL 过展示面的同一条脱敏律(凭据 / query 值不上,尾斜杠折掉)。
504
+ ⚠️ 无鉴权面:令牌、运维名单、worker 名、中心凭证都**不上**。钉:`test/health-self-report.test.ts`。
491
505
  - 模型拒因(**7.103.0+**,S-768):`/health.modelRefusals` 与 fleet 心跳 `modelRefusals` 同一只取值口、同一个对象(形 = settings-schema
492
506
  6.2.0 `ModelRefusalsSignal`:`entries` = 目录条目名 → 拒因码,`plane` = 模型面组拒码;值只码)。描述最近一次被判的中心候选目录;
493
507
  没有可报的拒 ⇒ 键缺席。名字是秘密形 / 空串 / `__proto__` 的不可用条目不上这两处(出口过名筛;引擎内部照旧拒它)。消费方读 `version`
@@ -657,10 +671,19 @@ SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机
657
671
  # limits / 审批名单 …)在新身份第一份候选到达那一拍换代。「换账号即降档 + 当场拉取」候 S-667(不在 7.100.0)。
658
672
  # 用户 JWT 形(CLI 登录)⇒ skill 正文 / prompt 产物走 /api/v1/me/…;盘上 LKG 与 prompt 状态按「worker + 凭证身份」分箱。
659
673
  SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
674
+ # SEMA_REGISTRY_LIVE_URL=http://<中心活性入口>:3100 # 7.121.0:fleet **活性组**(心跳 / 注销、服务租约 —— settings-schema FLEET_LIVE_PATHS)打的中心基址。
675
+ # 给「管理面在就绪门后、活性面另走一条不看就绪的入口」的部署(k8s:第二只 Service 带
676
+ # publishNotReadyAddresses,Ingress 的活性规则指它)。缺席 = SEMA_REGISTRY_URL。用量、配额租约、
677
+ # 拉配置始终打 SEMA_REGISTRY_URL。中心基址三席(SEMA_REGISTRY_URL / _LIVE_URL / _WEB_URL)同一条判据:
678
+ # 可解析的 http(s) URL,不带凭据 / query / fragment(基址后面要拼路径),坏形拒启(没接中心也拒)。
679
+ # SEMA_REGISTRY_WEB_URL=https://<中心 web 面> # 7.121.0:这个中心 **web 面**的固定基址,引擎原样转述到 `/health` 的 `center.webBaseUrl`
680
+ # (壳按它拼会话深链,路径怎么拼归壳)。缺席即缺席,引擎不拼不猜。形判同上(三席一条判据);
681
+ # 只在接了中心(SEMA_REGISTRY_URL)时挂。
660
682
  SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
661
683
  FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启公告环(announce/heartbeat/注销:在中心注册成可路由实例)。
662
- # 必须是可解析的 http(s) base URL——host:port 等坏形 5.13.0 起 boot 响亮拒
663
- # (此前静默每拍 announce 400,worker 永不注册)。
684
+ # 必须是可解析的 http(s) base URL、不带 userinfo / query / fragment(与中心基址三席同一条判据)——
685
+ # host:port 等坏形 boot 响亮拒(拒启码 config.fleet_advertise_address_invalid;
686
+ # 7.121.0 起无条件解析,没接中心也拒;空串 = 未设)。公告环、成员围栏判官、/health.center 都读这一份。
664
687
  # 🔴 7.106.0 起(S-866 ④)用量批报**不再**看本键:中心三件(URL + 凭证 + SEMA_REGISTRY_WORKER)
665
688
  # 配齐即每 60s 向中心 POST /api/fleet/usage(不公告的引擎也报用量;中心据此对租约单调对账)。
666
689
  # 不想报就不配中心三件。caps `centerWiring.usageReport` 可读。
@@ -671,6 +694,26 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启公
671
694
  # ⇒ true,单用户 ⇒ false;显式值压过派生。布尔八词,词表外拒启。409 not-lease 与耗尽 429 不受它管。
672
695
  # false = 7.105.0 及以前的 AP 姿态(照旧放行,warn fleet_lease_*_fail_open + 计数)。
673
696
  # 生效值 caps `centerWiring.failMode`;只在装了租约管理器的部署上有意义(lease:"absent" 不参与)。
697
+ # FLEET_GENERATION=7 # 7.121.0:fleet 成员的代际(配置渲染器写,手配只在你自己就是渲染器时)。规范十进制串(空串 / 前导零 /
698
+ # 符号 / 空白拒启);缺席 = 非 fleet 成员(单机形:不续服务租约、不受围栏)。回声到心跳与 /health 的
699
+ # generation(连同 leaseState / leaseUntil);排空口按它比 expectedGeneration。
700
+ # 服务租约只在中心于心跳答复里宣告 capabilities.servingLease 之后生效;没宣告 ⇒ leaseState none,
701
+ # 零续租零 503。🔴 设了它就必须配齐中心三件(URL + 凭证 + SEMA_REGISTRY_WORKER)与 FLEET_ADVERTISE_ADDRESS
702
+ # (不公告就听不到能力位),缺一 ⇒ 起服拒启 config.fleet_member_unfenced —— 成员不许悄悄脱离围栏。
703
+ # 续租打 POST /api/fleet/live/serving-lease(契约 FLEET_PATHS.servingLease;活性基址见 SEMA_REGISTRY_LIVE_URL)。
704
+ # 心跳答复体读不出 / 不是 JSON 对象 = 没有能力读数(位与租约钟原样,warn fleet_announce_reply_unreadable)。
705
+ # FLEET_SCOPE=acme # 7.121.0:本引擎所在的**租户 scope**(配置渲染器为多租引擎写 = 它的 per-worker 令牌绑的 scope)。缺席 =
706
+ # 缺省 scope global;在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启
707
+ # config.fleet_scope_invalid)。与 FLEET_GENERATION 不联动。只进排空口的身份比对:请求的 scope 与它比,
708
+ # 请求不带 scope 读成 global。续租体与心跳不带它(scope 由中心凭证绑定)。
709
+ # FLEET_LEASE_OUTAGE_MAX_MS=86400000 # 7.121.0:服务租约**中心不可达的容忍上限**(缺省 24 h;< 60000 / 0 / 非整数拒启)。租约过期(中心
710
+ # 不可达 / 答 unknown)后照常服务,过期满这么久 ⇒ 新工作 503 state.lease_expired(Retry-After 60;
711
+ # 取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻);中心恢复授予即恢复。
712
+ # 中心以正面证据拒(被取代 / 已退役)不受它管:当拍排空、到期自停,数据根记一只
713
+ # fleet-serving-lease-denied.json,同 worker 同代际重启后仍记住、第一条心跳就是排空态(换代由中心做,
714
+ # 不要手删)。那只文件写不进(数据根只读 / 盘满)⇒ 本进程照拒照停,但 error 行
715
+ # fleet_serving_lease_denial_not_persisted + fail-open 计数 server.fleet.denied-marker-unpersisted:
716
+ # 重启后不记得,在第一次续租答复之前会接单(中心那时再拒)。
674
717
  ```
675
718
  - 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
676
719
  - **逐条容错**(S-704 ②,7.101.0 起):目录里一条条目不合形(如 `contextWindow:"abc"`)⇒ **只丢那一条**(warn `config_entry_dropped`,
@@ -1170,10 +1213,13 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s:成孤儿之后还要静多久
1170
1213
  生效值**为准:`GET /v1/config/catalog` 里 `DRAIN_GRACE_MS` 那一行的 `effectiveValue` 就是本进程真在用的
1171
1214
  数(它读的是活 config,不是这份文档里的默认值),启动期的夹取/警告行同源。本文档其余各处谈这条排空腿
1172
1215
  时都不带数字,数字只有这一处 —— 照抄默认值去算壳形预算会差一个数量级。
1173
- - **排空期的第二发信号(`DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 `250`,7.102.0+)**:排空开始后再来一发 `SIGTERM` /
1174
- `SIGINT`,只有与排空起点相隔 ≥ 这个毫秒数才算「别等了,硬停」;更近的是同一拍被投递了两遍(壳心跳误判、编排器
1175
- 重试),打一行 `drain_duplicate_signal_ignored`(带 `signal` / `sinceFirstMs` / `minGapMs`)后照常排空。真要立刻
1176
- 停:隔过这个间隔再发一次,或 `SIGKILL`;设 `0` 回到「每个第二发都硬停」的旧行为。合法域 `[0, 10000]` 整数毫秒
1216
+ - **排空期的第二发信号(`DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 `250`,7.102.0+)**:排空中再来一发 `SIGTERM` /
1217
+ `SIGINT`,只有与**本次排空的第一发停止信号**相隔 ≥ 这个毫秒数才算「别等了,硬停」;更近的是同一拍被投递了两遍(壳心跳误判、
1218
+ 编排器重试),打一行 `drain_duplicate_signal_ignored`(带 `signal` / `sinceFirstMs` / `minGapMs`)后照常排空。SIGTERM 进的排空,
1219
+ 那一发就是第一发。经排空口 `POST /v1/admin/drain`(或服务租约被拒、附着租约自退)进的排空还没有停止信号:到达的第一发被**吸收**
1220
+ (期限仍是入口定死的那一个,info `drain_signal_after_request_drain_ignored`),再隔 ≥ 这个毫秒数的一发才硬停(7.121.0 起;
1221
+ 修前那一发被读成第二发)。k8s 例:`preStop` 调排空口 → kubelet 发一发 SIGTERM(吸收)→ `terminationGracePeriodSeconds` 到点 SIGKILL。
1222
+ 真要立刻停:隔过这个间隔再发一次,或 `SIGKILL`;设 `0` 回到「每个第二发都硬停」的旧行为。合法域 `[0, 10000]` 整数毫秒
1177
1223
  (上界 = `DRAIN_GRACE_MS` 的地板),坏值拒启。不在排空时 `SIGINT` 仍是立即硬停。
1178
1224
  - **自退留痕 `<数据根>/exit-last.json`**:自退在 **drain 开始那一刻**就把判据写进数据根(`orphan` /
1179
1225
  `reason` / `idleForMs` / `attached` / `inflight` / `lingerMs` + `version` / `at` / `pid`)。壳的 exit
@@ -1404,6 +1450,7 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1404
1450
  | `scenario` | — | 省略时取本部署的 `DEFAULT_SCENARIO`(工厂缺省场景名 = **`code`**——CC 编码 persona 蒸馏版,default 全量工具面,终验旋钮独立;`default` 场景**已非出厂缺省**,要用它须显式传 `scenario:"default"`,或部署方显式设 `DEFAULT_SCENARIO=default`)。可选内建名:`default`(通用/助理场景,域中性 persona,CC agent loop 全量工具面)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用;与 `code-review`/`discuss` 同属 clone-free 无执行环境场景,见 §5 末)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力写死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计)。**缺省与指派都必须是本部署在册的场景名,悬空不回落 `default`**:`DEFAULT_SCENARIO` 不在册 ⇒ **起服拒**(拒句点名那个名字与在册词表);中心按主体指派的场景名本部署没有 ⇒ 该主体的 run 答 **503 `state.scenario_assignment_unknown`**(体 `detail: { scenario, source: "ruling" }`,不带 `Retry-After`;修法 = 中心改指派,或把那个场景加回本部署,然后原样重试);请求体显式给的未知名仍是 400 `scenario_unknown` |
1405
1451
  | `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
1406
1452
  | `systemPrompt` / `appendSystemPrompt` | — | 调用方自带的系统提示 / 追加段(各 ≤16384 字符,超限 400 `request.field_invalid`;`appendSystemPrompt` 必须非空串)—— 任务自带时优先于角色 / 全局缺省(见 §模型角色) |
1453
+ | `outputStyle` / `settings.outputStyle` | — | 输出风格**两个座位、两件事**:风格的**正文**(指南全文)走 `settings.outputStyle`(折进系统提示的追加段);**每轮一句**走顶层 `outputStyle: { name, turnReminder? }`(7.121.0+,能力位 `outputStyle`)—— 原样交引擎,引擎每个回合用自己的标记帧说「`<name> output style is active. <turnReminder>`」(`turnReminder` 缺席用引擎缺省句),流上回声 `steering_injected` `source:"output_style"`。只在非缺省风格时声明顶层键;它的值形由引擎判(`name` 非空、≤256 字;`turnReminder` 在场即非空串;不许别的键),坏值这一轮终态 `failed`、码 `config.output_style_invalid` |
1407
1454
  | `model` | — | 本次用哪个模型:只能点服务端**已配置目录**里的名字 / 档位词 / 模型 id(目录见 `GET /v1/models`;请求体带不进新模型)。未知 ⇒ 400 `request.unknown_reference`(带 `available` 列表);目录里有、但本部署服务不了 ⇒ 400 `request.model_unavailable`;空串 / 非字符串 ⇒ 400 `request.field_invalid` |
1408
1455
  | ~~tools~~ | 🚫 | **不是受理键**(传了 ⇒ 400 `request.body_shape`,`unknownKeys` 点名)——工具面走场景装配;按请求只能**减**:`excludeTools` / `settings.permissions` 的 deny · ask(`allow` 7.115.0 起是 allow 规则:给出「有人表态」,回答没人表态的服务器工具 / A2A 调用;不加工具、不放掉任何一层自己的问) |
1409
1456
 
@@ -1530,12 +1577,12 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1530
1577
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
1531
1578
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方锁定自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
1532
1579
  | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
1533
- | `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。设计给编排层(k8s `preStop` / systemd `ExecStop` 等)在发 SIGTERM **之前**调用声明因由;⚠️ **如实**:本仓四条消费树(server/cli/sdk/web-admin)与编排层脚本树 `sema-deploy` 里对它零调用,`sema-comms audits/.md:213` N-18 已把它书面登记为 declined(cli 判定这是 operator 面,不是壳自动打的口)—— 今天它是给**运维手动 curl**(或运维自己写的编排脚本)用的口,不是任何一个内置壳自动调用的。调用后 server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。无 drain 面装配(嵌入宿主 / 测试)上声明无处可落 ⇒ 501 `capability.drain_state_required`。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
1580
+ | `POST /v1/admin/drain` `{"actionId":"…","worker":"…","deadlineAt":…}` | **排空口**(7.121.0 起体 = settings-schema `DrainRequest`;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 ⇒ 501)。体 `{ actionId, worker, deadlineAt, scope?, expectedGeneration?, instanceId?, term? }`:`actionId` 幂等键(人手形自取,如 `manual:<uuid>`)、`worker` 本引擎的 worker 名(`SEMA_REGISTRY_WORKER`)、`deadlineAt` 到期自停的 epoch ms、`scope` 本引擎的租户 scope(`FLEET_SCOPE`;不带 = 缺省 scope `global`,与没设 `FLEET_SCOPE` 的引擎相符 —— 多租引擎要带它自己的)、`expectedGeneration` fleet 成员的代际(`FLEET_GENERATION`;非成员不带)、`instanceId` 本进程身份(`GET /health` 读;不带则不比)。身份逐字段比(`worker` / `expectedGeneration` 缺席对缺席),不符 ⇒ `409 conflict.fleet_identity`(什么都不变);首次 ⇒ `200 {actionId, since, deadlineAt, state:"draining"}` 并**进入排空**(拒新提交、本副本可抢占的在飞腿当拍让出、活的审批卡改路耐久停驻、在飞归零或到期限即停);再来 ⇒ `200 … state:"already_draining"` 回显第一次的 `since` / `deadlineAt`,不重置。体坏 ⇒ `400 request.field_invalid`;嵌入宿主(无收尾链)⇒ `501 capability.drain_state_required`。没有 worker 身份的引擎(没配 `SEMA_REGISTRY_WORKER`)恒 409 —— 用 SIGTERM。经本口进的排空之后,平台随后发的第一发 SIGTERM 被吸收、不缩短期限(第二发判据见「排空期的第二发信号」)。7.120.x 的 `{"reason":"…"}` 声明形与 `/health.drainReason` 已退役。 |
1534
1581
  | `GET /v1/config/catalog` | **配置目录自描述**(operator-only —— `explicitOperatorOk`,空名单=谁都不是)。逐 env 键一行:`{name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, staticDefault?/derivedDefaultNote?, effectiveValue, valueClass, danger, center?{domain,path,precedence,timing,restartSlice?,managedNow,effectiveNow}}`(🔴 7.106.0 起 `center.domainExists` 删除:座位在加载期对契约 `DOMAIN_SCHEMAS` 核过域 + 路径,核不过即拒启 ⇒ 有 `center` 块 ⇔ 中心可写;readFace 四键 / `PERMISSIONS_DISABLE_AUTO_MODE` / `MODEL_DEGRADE_ON` / `_REACTIVE` 七行的座位随之删除,S-866 ⑤a);顶层带 `restartSlices`(重启片闭集)与 `centerDomains`(settings-schema 现行域表)。**两轴不混**:`writeLanes`=谁能写,`effectiveLane`=现在谁在生效(env-wins 族 env 占位 ⇒ center 腿 `effectiveNow:false`)。secret 类值**永不回传**(只回在场位;opaque 回在场位+长度;URL 剥 userinfo)。完备性由仓内 AST 双向对账门执法(分母=全 `src/` 生产 env 读取点)。旋钮 `CONFIG_CATALOG_ENABLED=false`(合规部署)⇒ 路径 404 + 能力位 `configCatalog:false`。响应 `Cache-Control: no-store`。**7.57.0 起**:`enum` 行的 `effectiveValue` **恒落在该行自己的 `enumValues` 闭集内**(仓内机器门执法)——此前 `SESSION_BACKEND` 回的是**内部标签** `tidb`(自 5.0.0 起是**退役公名**,写回去 boot 拒),现归一成公名 `mysql`;`MODEL_DEFAULT_THINKING` 补上一直被解析器接受、却漏在目录闭集外的 `xhigh`,并改为回**实算值**(`off`/未设 ⇒ `null`)而不是 env 原文。 |
1535
1582
  | `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
1536
1583
  | `GET /health` | 健康(无需鉴权)。**7.67.0+** 恒带 `startedAt`(引擎进程起点 epoch ms,一次铸;与 `pid` 并列的换代锚——值变了就是换了一条命)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
1537
1584
  | `GET /v1/sessions/:id/background` · `POST /v1/sessions/:id/background/stop` `{"includeRetained":true\|false}` | **会话后台任务的读口与停止口**(**7.102.0+**,S-745;能力位 `background.listFace` / `background.exitFaces`,老 server 404)。读口列**本会话**、**本副本**上的后台任务行(`{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained? }] }`,`retained:true` = 声明了保留的活服务,正常退出不会停它);停止口「软 → 等一次 `HOST_BG_KILL_GRACE_MS` → 硬」收掉本会话的后台 shell(SIGTERM 处理器先有一次宽限跑完,之后组 SIGKILL,屏蔽 SIGTERM 的循环也活不过去;有后台行时应答慢一次宽限)并逐字返回受据 `{ receipts: [{ id, outcome }] }`:`killed` / `already_gone` / `retained_skipped`(`includeRetained:false` 留下的活服务)/ `no_pgid`(远端 / 沙箱车道没有本机进程组可发,已派普通 kill,「可能还在跑」;host 车道的 `already_gone` 只由观察得出 —— 引擎已收割组长或内核答组已空,7.103.0 起,S-769;darwin 上梯子那一枪先落地、引擎还没收割的那一拍如实答 `no_pgid`,判「收掉没有」读进程)。`includeRetained` 必填:`true` = 连活服务一起停(壳的「Exit and stop tasks」),`false` = 活服务照留(`sema kill`)。鉴权 = 会话属主或显式 operator(未知与他人同 404);停止口在无凭据写门内(没配 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` ⇒ 503 `auth.service_token_required`)。`DELETE /v1/sessions/:id` 走同一只收敛(`includeRetained:false`);run 取消(Esc)**不**收后台 shell。契约 §16 |
1538
- | `GET /readyz` | **就绪探针**(**7.102.0+**,无需鉴权)。就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`,`reasons` ⊆ {`draining`, `model_roster_pending`}(按优先序)。与 `/health` 的 `ready` / `readyReason` / `reasons` 同一只判;库连不上**不算**未就绪(`storeLive` 只在 `/health` 披露)。给只认状态码的**摘流**探针用(k8s `readinessProbe.httpGet`、LB 健康检查;重启语义的 `HEALTHCHECK` / `livenessProbe` 留在 `/health`),见 §0「探针」 |
1585
+ | `GET /readyz` | **就绪探针**(**7.102.0+**,无需鉴权;7.121.0 起是唯一的探针路径)。就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`,`reasons` ⊆ {`draining`, `model_roster_pending`}(按优先序)。与 `/health` 的 `ready` / `readyReason` / `reasons` 同一只判;库连不上**不算**未就绪(`storeLive` 只在 `/health` 披露)。给只认状态码的**摘流**探针用(k8s `readinessProbe.httpGet`、LB 健康检查、镜像 `HEALTHCHECK`);存活探针用 TCP 探端口,不打 HTTP,见 §0「探针」 |
1539
1586
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
1540
1587
 
1541
1588
  > **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
@@ -71,8 +71,8 @@ spec:
71
71
  - { name: K8S_RUNTIME_CLASS, value: {{ .Values.sandbox.runtimeClass }} }
72
72
  - { name: K8S_SANDBOX_IMAGE, value: {{ .Values.sandbox.image | quote }} }
73
73
  - { name: K8S_TOKEN, valueFrom: { secretKeyRef: { name: sema-runner-token-copy, key: token } } }
74
- readinessProbe:
75
- httpGet: { path: /health, port: 8090 }
74
+ readinessProbe: # the only probe path is /readyz (/health is diagnostic, always 200)
75
+ httpGet: { path: /readyz, port: 8090 }
76
76
  initialDelaySeconds: 5
77
77
  periodSeconds: 5
78
78
  ---
@@ -84,8 +84,8 @@ spec:
84
84
  {{- end }}
85
85
  - { name: K8S_SANDBOX_IMAGE, value: {{ .Values.sandbox.image | quote }} }
86
86
  - { name: K8S_TOKEN, valueFrom: { secretKeyRef: { name: sema-runner-token-copy, key: token } } }
87
- readinessProbe:
88
- httpGet: { path: /health, port: 8090 }
87
+ readinessProbe: # the only probe path is /readyz (/health is diagnostic, always 200)
88
+ httpGet: { path: /readyz, port: 8090 }
89
89
  initialDelaySeconds: 5
90
90
  periodSeconds: 5
91
91
  ---
@@ -185,7 +185,7 @@ spec:
185
185
  # 默认=公开池 code-node(小巧 git+node+python;clay 拍 2026-07-13,重任务 per-task profile 按需选)——裸 OS 镜像无 pkg-source hook,别退回
186
186
  - { name: K8S_SANDBOX_IMAGE, value: "docker.io/claybobby/code-node:20260707-749ab84f5fdd-786f5ef10ffd" }
187
187
  - { name: K8S_TOKEN, valueFrom: { secretKeyRef: { name: sema-runner-token-copy, key: token } } }
188
- readinessProbe: { httpGet: { path: /health, port: 8090 }, initialDelaySeconds: 5, periodSeconds: 5 }
188
+ readinessProbe: { httpGet: { path: /readyz, port: 8090 }, initialDelaySeconds: 5, periodSeconds: 5 } # the only probe path (/health is diagnostic)
189
189
  ---
190
190
  apiVersion: v1
191
191
  kind: Service
@@ -14,6 +14,7 @@ import type { UnpricedCeilingVerdict } from "./unpriced-ceiling-verdict.js";
14
14
  import type { ConsolidationSeatWatch } from "./memory-consolidation.js";
15
15
  import type { DegradeRouteWatch } from "../brain.js";
16
16
  import { type EngineRestartSignal } from "../config-center/facade.js";
17
+ import { type ConfigHandoff } from "../fleet/engine-signal.js";
17
18
  import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
18
19
  import type { CenterPullView } from "../config-center/pull-state.js";
19
20
  import type { LimitSync } from "./limit-sync.js";
@@ -108,17 +109,14 @@ export interface ConfigCenterRuntime {
108
109
  onSelfReportChange(listener: (changed: SelfReportChange) => void): void;
109
110
  /** /health.restart 与 fleet 心跳 `restart` 的**同一只**取值口(orchestrator 自动重启信号;settings-schema `RestartSignal` 形)。 */
110
111
  restartState(): EngineRestartSignal | undefined;
112
+ /** `/health.configHandoff` 的取值口(只上 `/health`):在场 ⇔ 部署声明了耐久接力(`CONFIG_LKG_DURABLE` ∧ LKG 启用 —— 判「handoff confirmed」的
113
+ * 同一只谓词)∧ 应用着的那一代 ≠ 最后确认耐久接力的那一代(接力账;合成律 `buildConfigHandoff`)。非耐久部署 / 本地腿 / 没有配置管道 ⇒ 缺席。
114
+ * 两个事实两张脸:「要重启但扣着」归 `restart.withheld`,「声明了耐久接力而这一代没确认」归本键。 */
115
+ configHandoffState(): ConfigHandoff | undefined;
111
116
  /** S-768:`/health.modelRefusals` 与 fleet 心跳 `modelRefusals` 的**同一只**取值口(settings-schema 6.2.0 `ModelRefusalsSignal` 形:
112
117
  * 名 → 码 + 组拒码)。描述**最近一次被判的那份中心候选目录**(每次候选 apply 的报告 `ApplyReport.modelRefusals` 投影;组拒时就是被拒
113
118
  * 的那份 —— 引擎自己的拒因表不换代)。没有可报的拒 / 没有配置管道 / 还没判过候选 ⇒ `undefined`(键缺席)。值变了 ⇒ 通知自报订阅者。 */
114
119
  modelRefusalsState(): ModelRefusalsSignal | undefined;
115
- /** /health.modelPlaneDeferred:被扣着、还没有 next-boot handoff 的候选(codex R18 起的状态机)。S-668 波 0 起
116
- * 模型面不再 defer,键名保留,今天承载的是 durable LKG 发布债与 skills 债(见 `planeDeferredNoHandoff`)。 */
117
- planeDeferredState(): {
118
- version: number;
119
- since: number;
120
- blocked?: string[];
121
- } | undefined;
122
120
  /** [ref] 世代账:/health 的 target/applied/失败持续时长三键 + 诊断端点的 configApply 段(同一份读数)。
123
121
  * `undefined` = 本部署没有配置管道(纯 env worker)⇒ 两个读面的形状**逐字不变**(additive 纪律)。 */
124
122
  configApplyState(): ConfigApplyLedgerView | undefined;
@@ -11,7 +11,7 @@ import { applyCenterPlugins } from "../capabilities/center-plugins.js";
11
11
  import { applyCatalogToSource, withPromptArtifactBackfill } from "../capabilities/center-prompts.js";
12
12
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
13
13
  import { centerScenarios, centerScenarioDetails } from "../capabilities/scenarios.js";
14
- import { configLkgEnabled, isRosterGateArmed } from "../config.js";
14
+ import { isRosterGateArmed } from "../config.js";
15
15
  import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg, legacyV1LkgPath, migrateV1Lkg } from "../config-lkg.js";
16
16
  import { answerStillCurrent, centerStateBin, credentialIdentityNow, judgeCenterCredentialAtBoot } from "../center-credential.js";
17
17
  import { buildCenterCredentialHealthView, createCenterCredentialRenewer } from "../center-credential-renewer.js";
@@ -20,6 +20,7 @@ import { createConfigProvider, raceBootFetch, serviceConfigSourceKind, BOOT_FETC
20
20
  import { createKeyResolver } from "../key-resolver.js";
21
21
  import { buildSealedKeyPublicFace, ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
22
22
  import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, fullyLandedForLkg, fetchPromptArtifact } from "../config-center/facade.js";
23
+ import { buildConfigHandoff, buildRestartSignal, configHandoffMissing } from "../fleet/engine-signal.js";
23
24
  import { createMcpRevocationLedger, enabledMcpEntries } from "../config-center/mcp-revocation.js";
24
25
  import { SHA256_HEX_RE } from "../digest-form.js";
25
26
  import { BOOT_REJUDGE_OF_REFUSED_GROUP, createConfigApplyLedger, modelPlaneCommitted, refusedGroups } from "../config-center/apply-ledger.js";
@@ -315,9 +316,27 @@ export async function createConfigCenterRuntime(ctx) {
315
316
  }
316
317
  };
317
318
  let bootConfigPending;
318
- const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
319
+ const lkgEnabled = configProvider?.kind === "remote" && config.configLkgEnabled;
319
320
  const lkgPathFor = (identity) => config.configLkgPath ?? defaultLkgPath(config, cc?.worker, identity);
320
321
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
322
+ let handoff = {};
323
+ let handoffMissingSince;
324
+ const latchHandoff = () => {
325
+ const bootIdentity = currentIdentity();
326
+ const ledger = { ...handoff, ...(bootIdentity !== undefined ? { bootIdentity } : {}) };
327
+ handoffMissingSince = configHandoffMissing(ledger) ? (handoffMissingSince ?? Date.now()) : undefined;
328
+ return ledger;
329
+ };
330
+ const noteHandoff = (next) => {
331
+ const applied = next.applied ?? handoff.applied;
332
+ const durable = next.durable ?? handoff.durable;
333
+ handoff = { ...(applied !== undefined ? { applied } : {}), ...(durable !== undefined ? { durable } : {}) };
334
+ latchHandoff();
335
+ };
336
+ const markApplied = (gen, version) => {
337
+ applyLedger.applied(gen, version);
338
+ noteHandoff({ applied: version });
339
+ };
321
340
  let lkgBooted = false;
322
341
  let bootLkgCandidate;
323
342
  let lastPrewarmBodiesVerified = false;
@@ -357,6 +376,8 @@ export async function createConfigCenterRuntime(ctx) {
357
376
  }
358
377
  lastPrewarmBodiesVerified = true;
359
378
  await saveLkg(lkgPath, cc?.worker, identity, effectiveObj, etag);
379
+ if (lkgSurvivesRestart)
380
+ noteHandoff({ durable: { version: effectiveObj.version, identity } });
360
381
  return lkgSurvivesRestart;
361
382
  }
362
383
  catch (err) {
@@ -417,7 +438,10 @@ export async function createConfigCenterRuntime(ctx) {
417
438
  recordApply(rep);
418
439
  },
419
440
  });
420
- applyLedger.applied(lkgGen, lkgR.effective.version);
441
+ const lkgIdentity = candidateIdentityOf(lkgR.effective);
442
+ if (lkgSurvivesRestart && lkgIdentity !== undefined)
443
+ noteHandoff({ durable: { version: lkgR.effective.version, identity: lkgIdentity } });
444
+ markApplied(lkgGen, lkgR.effective.version);
421
445
  await adoptCenterPrompts(lkgR.effective, "boot-lkg");
422
446
  const lkgPlane = modelPlaneCommitted(lkgReport);
423
447
  if (lkgPlane)
@@ -485,7 +509,7 @@ export async function createConfigCenterRuntime(ctx) {
485
509
  recordApply(rep);
486
510
  },
487
511
  });
488
- applyLedger.applied(bootGen, r.effective.version);
512
+ markApplied(bootGen, r.effective.version);
489
513
  const bootClean = fullyLandedForLkg(r, bootReport);
490
514
  if (bootClean)
491
515
  ccEtag = r.etag;
@@ -604,9 +628,9 @@ export async function createConfigCenterRuntime(ctx) {
604
628
  onSelfReportChange: (listener) => {
605
629
  selfReportListeners.push(listener);
606
630
  },
607
- restartState: () => pendingRestart,
631
+ restartState: () => buildRestartSignal(pendingRestart, planeDeferredNoHandoff),
632
+ configHandoffState: () => (configProvider === undefined || configProvider.kind === "local" || !lkgSurvivesRestart ? undefined : buildConfigHandoff(latchHandoff(), handoffMissingSince)),
608
633
  modelRefusalsState: () => modelRefusals,
609
- planeDeferredState: () => planeDeferredNoHandoff,
610
634
  configApplyState: () => (configProvider ? applyLedger.view() : undefined),
611
635
  centerPullState: () => {
612
636
  const view = applyLedger.view();
@@ -622,7 +646,7 @@ export async function createConfigCenterRuntime(ctx) {
622
646
  },
623
647
  async applyCenterCapabilities(skills) {
624
648
  if (effective?.skills && config.configCenter) {
625
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir(config) : undefined);
649
+ skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, config.configLkgEnabled ? defaultSkillCacheDir(config) : undefined);
626
650
  }
627
651
  if (effective?.plugins !== undefined) {
628
652
  const pluginOut = await applyCenterPlugins(skills, effective, {
@@ -850,7 +874,7 @@ export async function createConfigCenterRuntime(ctx) {
850
874
  ceilingVerdict?.judge();
851
875
  mcpRevocations.applyEnabledEntries(enabledMcpEntries(r.effective.mcp));
852
876
  applyLiveReadFace(r.effective, swapRunnerDeps, "refresh");
853
- applyLedger.applied(tickGen, r.effective.version);
877
+ markApplied(tickGen, r.effective.version);
854
878
  const tickPlane = modelPlaneCommitted(applyReport);
855
879
  if (tickPlane)
856
880
  appliedPlaneEff = r.effective;
@@ -963,7 +987,7 @@ export async function createConfigCenterRuntime(ctx) {
963
987
  limitSync?.sync();
964
988
  ceilingVerdict?.judge();
965
989
  applyLiveReadFace(r.effective, swapRunnerDeps, "boot-deferred");
966
- applyLedger.applied(lateGen, r.effective.version);
990
+ markApplied(lateGen, r.effective.version);
967
991
  const latePlane = modelPlaneCommitted(applyReportLate);
968
992
  if (latePlane)
969
993
  appliedPlaneEff = r.effective;
@@ -36,7 +36,7 @@
36
36
  * 的请求是毫秒级的,而本腿 15s 采样一拍 —— 采样点几乎必然落在两次轮询之间,`attached` 与
37
37
  * `inflight` **同时读到 0**。只看那两条的实现会在一只**正被使用**的引擎底下把它关掉。租约的
38
38
  * 建立面按设计就是「首个 HTTP/SSE 接触」,不只是 SSE。
39
- * 4. **退出 = 走 `drain`**,即 `boot/shutdown.ts` 里 SIGTERM 用的那一条 `drainThenShutdown`:拒新提交
39
+ * 4. **退出 = 走 `drain`**,即 `boot/shutdown.ts` 里 SIGTERM 用的那一只唯一排空入口 `enterDrain`(编排器片 2 前名 `drainThenShutdown`):拒新提交
40
40
  * (503+Retry-After)→ 候在途腿跑完 → `hardShutdown`。in-flight run 的结算 / park 语义与人工停机
41
41
  * **逐字一致**。本模块自己**从不** `process.exit`。
42
42
  * 5. **crash-last 判别位**([ref] ④「flush 判别位标 auto-exit 非崩溃」):走上面那条受控退,
@@ -331,6 +331,7 @@ export function createResolveSpec(ctx) {
331
331
  objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
332
332
  systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
333
333
  appendSystemPrompt: acceptedAppend,
334
+ ...(body.outputStyle !== undefined ? { outputStyle: body.outputStyle } : {}),
334
335
  sessionId: auth?.sessionId,
335
336
  principal: auth?.principal,
336
337
  images: body.images,
@@ -26,11 +26,12 @@ import type { Logger } from "../observability/logger.js";
26
26
  import type { startOtlpExporter } from "../observability/otel-exporter.js";
27
27
  import type { CostQuota } from "../observability/cost-quota.js";
28
28
  import type { RateLimiter } from "../observability/rate-limit.js";
29
- import type { startFleetClientFromEnv } from "../fleet-client.js";
29
+ import type { startFleetClientFromConfig } from "../fleet-client.js";
30
30
  import type { BreakerStateStore, CostQuotaStore, RateLimiterStore, StoreBackend } from "../plugins/store-backend.js";
31
31
  import type { WorkflowNotifyJournalStore } from "../orchestration/workflow-notify-journal.js";
32
32
  import type { ToolApprovalCoordinator } from "../tool-approval.js";
33
33
  import type { ParkCommitWatch } from "../plugins/park-commit-watch.js";
34
+ import { type DrainState } from "../fleet/drain-entry.js";
34
35
  export interface ShutdownCtx {
35
36
  config: ServiceConfig;
36
37
  /** [ref]①:崩溃遗言的 version 键(与 /v1/capabilities.version 同源=main 的 serviceVersion())。 */
@@ -61,16 +62,10 @@ export interface ShutdownCtx {
61
62
  subRunner: Runner;
62
63
  lspManager: RunnerDeps["lspManager"];
63
64
  workflowNotifyJournal: WorkflowNotifyJournalStore | undefined;
64
- fleetClient: ReturnType<typeof startFleetClientFromEnv>;
65
+ fleetClient: ReturnType<typeof startFleetClientFromConfig>;
65
66
  backend: StoreBackend | undefined;
66
- drainState: {
67
- draining: boolean;
68
- since?: number;
69
- inflight?: () => number;
70
- lastActivityAt?: () => number;
71
- attachedStreams?: () => number;
72
- lastAttachEndedAt?: () => number;
73
- };
67
+ /** 编排器片 2 件 2:共享排空盒子(`fleet/drain-entry.ts`)—— 本函数往上装**唯一**入口 `enter`。 */
68
+ drainState: DrainState;
74
69
  /** [ref]-2:store 活体探针(main 建)——不停的话 hardShutdown 后仍对正在关闭的池发探针,刷假
75
70
  * store_probe_dead 告警(文件头契约 3 的同族漏网)。缺席 = 该部署形没建探针。 */
76
71
  storeLiveProbe: {
@@ -95,6 +90,10 @@ export interface ShutdownCtx {
95
90
  /** S-929 F1:checkpoint 店的 park 提交观察层(`plugins/park-commit-watch.ts`;在场 ⇔ 本部署有 checkpoint 店)。hardShutdown 在
96
91
  * `backend.close()` 之前等它 quiesce —— 在飞的 `put`(审批 park / 资源挂起…,与审批协调器在不在场无关)不许被抽掉连接池。 */
97
92
  parkCommits: ParkCommitWatch | undefined;
93
+ /** 编排器片 2 件 3:服务租约客户端(在场 ⇔ fleet 成员 ∧ 中心车道配齐)。hardShutdown 起点停它的续租定时器(文件头契约 3 同族)。 */
94
+ servingLease?: {
95
+ stop(): void;
96
+ } | undefined;
98
97
  }
99
98
  /**
100
99
  * 受控退的**后台宽限**还要等多久(ms)—— hardShutdown 软停后台 shell 之后、`process.exit` 之前,给软停过的行**一次**