@sema-agent/server 7.101.1 → 7.102.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 +23 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +58 -4
- package/dist/approval-card.d.ts +35 -2
- package/dist/approval-card.js +19 -1
- package/dist/background-session-reap.d.ts +55 -0
- package/dist/background-session-reap.js +90 -0
- package/dist/boot/config-center.d.ts +16 -4
- package/dist/boot/config-center.js +44 -15
- package/dist/boot/exit-boundary.js +17 -0
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +3 -4
- package/dist/boot/shutdown.d.ts +2 -1
- package/dist/boot/shutdown.js +33 -12
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.js +6 -4
- package/dist/boot/stage-08-reapers.d.ts +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +7 -3
- package/dist/capabilities/builtin-tools.d.ts +14 -1
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/center-credential-renewer.d.ts +76 -0
- package/dist/center-credential-renewer.js +359 -0
- package/dist/center-credential.d.ts +76 -22
- package/dist/center-credential.js +119 -43
- package/dist/config-catalog.js +9 -4
- package/dist/config-center/apply-effective.js +5 -12
- package/dist/config-center/center-request.d.ts +13 -0
- package/dist/config-center/center-request.js +23 -3
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/http-client.js +2 -5
- package/dist/config-center/read-warnings.d.ts +19 -48
- package/dist/config-center/read-warnings.js +7 -49
- package/dist/config-center/restart-signal.d.ts +8 -11
- package/dist/config-center/types.d.ts +4 -2
- package/dist/config-provider.js +1 -48
- package/dist/config-types.d.ts +77 -5
- package/dist/config-types.js +5 -0
- package/dist/config.js +49 -13
- package/dist/device-store.d.ts +1 -1
- package/dist/fleet-client.d.ts +21 -7
- package/dist/fleet-client.js +41 -12
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/http/approval-sweeps.js +5 -20
- package/dist/http/dispatch.js +20 -14
- package/dist/http/readiness.d.ts +87 -0
- package/dist/http/readiness.js +54 -0
- package/dist/http/resume-legs.js +5 -4
- package/dist/http/route-table.d.ts +7 -0
- package/dist/http/route-table.js +6 -0
- package/dist/http/routes/a2a-serve.js +6 -14
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/background.d.ts +80 -0
- package/dist/http/routes/background.js +94 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/server.d.ts +21 -8
- package/dist/http/server.js +2 -2
- package/dist/model-entry-refusal.d.ts +11 -10
- package/dist/model-entry-refusal.js +13 -10
- package/dist/observability/fail-open.d.ts +2 -2
- package/dist/observability/fail-open.js +2 -2
- package/dist/parked-decide.d.ts +11 -5
- package/dist/parked-decide.js +4 -1
- package/dist/parked-revive-surface.d.ts +47 -0
- package/dist/parked-revive-surface.js +20 -0
- package/dist/plugins/background-shell-support.d.ts +20 -1
- package/dist/plugins/background-shell-support.js +26 -0
- package/dist/plugins/host-platform.d.ts +11 -1
- package/dist/plugins/host-platform.js +11 -0
- package/dist/plugins/remote-env-host.d.ts +14 -1
- package/dist/plugins/remote-env-host.js +18 -5
- package/dist/tool-approval.d.ts +30 -1
- package/dist/tool-approval.js +31 -8
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/package.json +3 -3
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,29 @@
|
|
|
7
7
|
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
8
|
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
9
|
|
|
10
|
+
## 7.102.0(车OI 部分)—— core 7.32.0 提货带来的四条(后台 shell 退出硬收 / Esc 不收后台 / 读根授权与 artifact store / parked 继承工具新失败形)
|
|
11
|
+
|
|
12
|
+
- **后台 shell 在进程退出与会话删除时被硬收(core #1107,行为面)**:
|
|
13
|
+
- **进程退出**:屏蔽 SIGTERM 的后台循环、崩溃出口上宽限跑不完的后台进程,升级后在退出边界被组 SIGKILL 收掉(改前活过引擎,成孤儿)。
|
|
14
|
+
- **`DELETE /v1/sessions/:id`**:7.101.1 只走软梯子(SIGTERM → `HOST_BG_KILL_GRACE_MS` → 组 SIGKILL —— 屏蔽 SIGTERM 的 shell 在宽限后同样死);
|
|
15
|
+
7.102.0 起是「软 → 等一次宽限 → 硬」:SIGTERM 处理器照样有一次宽限跑完,宽限之后再补硬面一枪组 SIGKILL 并出受据。有后台行的会话,
|
|
16
|
+
DELETE 的应答在硬面之后返回(慢一次宽限,缺省 1000 ms)。
|
|
17
|
+
- **声明了保留的活服务不受影响**(`retained_skipped`)。会话 reap 不再「只结算不杀」env 级保留的行(core NARROWING:保留的行原样留着、行仍
|
|
18
|
+
`running`,别的一律杀)。
|
|
19
|
+
- **谁受伤**:① 依赖「引擎挂了后台进程还在跑」而又没有声明保留的部署 —— 迁移:给那条任务写 `retainBackgroundProcesses: true`(或用 env
|
|
20
|
+
级保留),或者干脆把服务交给进程管理器;② **受控退(SIGTERM 排空)时没有开着的连接** —— 退出很快落到退出边界,硬面的组 SIGKILL 紧跟
|
|
21
|
+
早收割的 SIGTERM,后台进程的**优雅退出可能被截断**(7.101.1 在这里只发 SIGTERM,进程作为孤儿把处理器跑完)。这是 core 7.32 契约
|
|
22
|
+
「每个退出终点软后硬」带来的;缓解(受控退在早收割 + 一次宽限之后再退出)候 S-758,本版不做。要保住优雅退出的长活服务,声明保留。
|
|
23
|
+
- **声明读根落在本地 artifact store 里 ⇒ Artifact 挂载拒(core #1093,部署面 NARROWING,本服务今天零影响)**:core 7.32.0 起
|
|
24
|
+
`additionalReadDirectories` / `additionalDirectories` 里**任何一条**在 artifact store 之内(或就是它、或包含它),挂载期
|
|
25
|
+
`config.artifact_host_invalid`(改前只拒「store 在根内」一个方向)。本服务今天**没有** artifact host,这条到达不了;将来接 host 时,别把
|
|
26
|
+
读根声明到 store 目录里。同批:读根按身份判 —— 声明的根在 run 首次 shell 判定前被换成软链,不再为链的目标作保。
|
|
27
|
+
- **parked 子代停在「继承工具」上的 `/decide` allow ⇒ 422 `parked_resume.startup_failed`(core #1110,新失败形,非回归)**:core 7.32.0 起
|
|
28
|
+
子代继承场景层工具,停在这类工具上的审批在赎回交不出继承座时被拒(审批留 pending,`deny` 可结)。本版对一层行交座(不再落这一形;前提是行的根会话现在仍属于这张卡的租户),孙代行 / 根会话已删或易主的行仍落
|
|
29
|
+
(core #1112 候)。7.31.x 的子代根本没有这些工具,所以这不是旧卡的回归。
|
|
30
|
+
- **类型面(本仓内部 API,三句)**:删 `ServiceDeps.parkedReviveTool` / `ParkedDecideDeps.reviveTool`,改 `parkedReviveSurface` /
|
|
31
|
+
`reviveSurface`(按行解析赎回面的工厂,零别名);受害 = 手铸这两个 deps 的嵌入方,tsc 红即通告;补偿 = 固定工具形 `() => ({ reviveTool })`。
|
|
32
|
+
|
|
10
33
|
## 7.101.0 —— 十四条(`run-local` 执法三张审批名单 / 人停下的 run 退 1 / 模型目录 fail-closed 三条 + 类型面 `ServiceConfig.modelRefusals` · `ServiceConfig.lspEnvAllow` / 记忆抹除面 409 撤 + 存量只读位一次性 `chmod`(滚动窗内会被旧副本再锁,起服 warn 点名)/ settings 层 `crossSessionInbound: accept` 改答 hold / 子进程剥名收窄 / SQL `session_meta` 新列 / 沙箱名单形错拒启 + 空串值照注入 / `WEB_SEARCH_ENDPOINT` 带 userinfo 拒启 / 配置目录两行 `csv` → `secret` / 被拒名撞档位别名 ⇒ 模型面组拒;S-690 / S-694 / S-699 / S-704 / S-705 / S-712 / S-713 / S-716 / S-723)
|
|
11
34
|
|
|
12
35
|
- **`run-local` 上 `APPROVAL_REQUIRE` / `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` 生效**(改前:这条一次性 CLI 腿不读三张名单,名单工具照跑,只有启动日志一条 `tool_policy_only_sensitive_baseline` warn)。改后与 HTTP 腿同一只策略:`APPROVAL_DENY` 命中 ⇒ 当场拒;`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ stdin 是 TTY 就 `y/N` 问人(问句多一行这次调用的参数),否则当场拒并在 stderr 点名三张名单键(`APPROVAL_NEVER_AUTO` 不吃 `APPROVAL_AUTO_BUDGET` 预算)。**谁受伤**:无 TTY(CI)跑 `run-local`、而 `.env` 与服务共用并带着名单的脚本 —— 名单工具从「照跑」变「当场拒」。**迁移**:给 CLI 环境单独一份 `.env`(不带名单),或设 `APPROVAL_AUTO_BUDGET=<N>` 让 require 名单前 N 次自动批,或在终端里跑(TTY 问人)。`tool_policy_only_sensitive_baseline` 在 `run-local` 上不再出现(名单已执法)。
|
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ One row per endpoint family (not exhaustive):
|
|
|
230
230
|
|
|
231
231
|
| Endpoint family | What it serves |
|
|
232
232
|
|-----------------|----------------|
|
|
233
|
-
| `GET /health` · `GET /metrics` | Liveness + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
|
|
233
|
+
| `GET /health` · `GET /readyz` · `GET /metrics` | Liveness, readiness (`/readyz`: 200 / 503 for status-code-only probes such as a k8s `readinessProbe`) + Prometheus metrics (`/metrics/summary`, `/metrics/plan-cache`) |
|
|
234
234
|
| `GET /v1/capabilities` | Deployment capability discovery — what this deployment can actually do, so clients never probe 501s |
|
|
235
235
|
| `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | Per-server MCP status with **no run required**: dial each declared server, list its tools, close it again — the first for this deployment's own declarations, the second for a caller's `.mcp.json`. Rows are the engine's own wiring-manifest rows, so a one-shot command and an interactive session read the same verdict. Rate-capped per caller and cached briefly (it really dials) |
|
|
236
236
|
| `GET /v1/models` | Model catalog (names only; no gateway URLs or keys) |
|
package/README.zh-CN.md
CHANGED
|
@@ -197,7 +197,7 @@ Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`
|
|
|
197
197
|
|
|
198
198
|
| 端点族 | 服务什么 |
|
|
199
199
|
|--------|----------|
|
|
200
|
-
| `GET /health` · `GET /metrics` | 存活探针 + Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
|
|
200
|
+
| `GET /health` · `GET /readyz` · `GET /metrics` | 存活探针 + 就绪探针(`/readyz`:200 / 503,给只认状态码的探针,如 k8s `readinessProbe`)+ Prometheus 指标(`/metrics/summary`、`/metrics/plan-cache`) |
|
|
201
201
|
| `GET /v1/capabilities` | 部署能力发现 —— 本部署真正能做什么,客户端免 501 探测 |
|
|
202
202
|
| `GET /v1/capabilities/mcp` · `POST /v1/capabilities/mcp/probe` | **无需先跑一条 run** 的逐台 MCP 状态:现连、列工具、关掉 —— 前者答本部署自己申报的服务器,后者答调用方自带的 `.mcp.json`。行就是引擎装配名册里的那一行,所以一次性命令与交互会话读到同一个判定。它会真拨号,所以按调用方限速 + 短窗复用 |
|
|
203
203
|
| `GET /v1/models` | 模型目录(仅名字,不含网关 URL/key) |
|
package/USAGE.md
CHANGED
|
@@ -48,6 +48,25 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
|
|
|
48
48
|
|
|
49
49
|
> 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
|
|
50
50
|
|
|
51
|
+
**探针:存活看 `/health`,就绪看 `/readyz`(7.102.0+)**。`/health` 恒 200(liveness:进程在答);只认状态码的**摘流**
|
|
52
|
+
探针(k8s `readinessProbe` / Traefik / 负载均衡健康检查)读不到它体里的 `ready:false`,要摘流就指 `GET /readyz`:就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`。
|
|
53
|
+
`reasons` 闭集两词:`draining`(SIGTERM 后优雅排空中)、`model_roster_pending`(`CONFIG_REQUIRE_ROSTER=true`
|
|
54
|
+
且中心模型名册尚未落地)。两者都免鉴权(与 `/health` 同位,探针带不了 token)。🔴 **库连不上不算未就绪**:
|
|
55
|
+
`storeLive:false` 只在 `/health` 体里披露 —— 一次库抖若进就绪门,整队副本会在同一拍被摘光(共模失效)。
|
|
56
|
+
⚠️ **重启语义的探针别指 `/readyz`**(镜像 `HEALTHCHECK` 配 Swarm / autoheal、k8s `livenessProbe`):中心宕机时整队副本
|
|
57
|
+
同卡 `model_roster_pending`,指 `/readyz` 就会一起被重启、再一起卡住 —— 同一种共模失效。它们留在 `/health`(本仓
|
|
58
|
+
`Dockerfile` 的 `HEALTHCHECK` 就是 `/health`)。
|
|
59
|
+
```yaml
|
|
60
|
+
# k8s:liveness 打 /health(进程死才重启),readiness 打 /readyz(不就绪只摘流,不重启)
|
|
61
|
+
livenessProbe:
|
|
62
|
+
httpGet: { path: /health, port: 8090 }
|
|
63
|
+
periodSeconds: 10
|
|
64
|
+
readinessProbe:
|
|
65
|
+
httpGet: { path: /readyz, port: 8090 }
|
|
66
|
+
periodSeconds: 5
|
|
67
|
+
failureThreshold: 1 # 排空 / 名册未落要立刻摘,不等三拍
|
|
68
|
+
```
|
|
69
|
+
|
|
51
70
|
**可选 — 多网关 failover + Anthropic 路由(消除单网关 SPOF)**
|
|
52
71
|
```bash
|
|
53
72
|
# ① 同协议冗余:主网关挂(连不上/上游开始前就失败)→ 按序切到备网关(同一 model id)
|
|
@@ -88,6 +107,9 @@ MODEL_CIRCUIT_BREAKER=true MODEL_CB_FAILURE_THRESHOLD=5 MODEL_CB_COOLDOWN_MS=3
|
|
|
88
107
|
# ⑤ 每次调用的重试上限(0–20)。**不设 = 用引擎默认**(server 不再写死它)
|
|
89
108
|
GATEWAY_MAX_RETRIES=10 # openai 兼容腿(第三方限流 provider 走的就是这条)
|
|
90
109
|
ANTHROPIC_MAX_RETRIES=10 # 云 Anthropic 腿
|
|
110
|
+
# ⑥ 慢后端上开 auto 模式(permissionMode:"auto"):分类器那一级也在等同一个排队 + prefill 的后端,到点没裁 ⇒ 这次工具调用被拒。
|
|
111
|
+
# 按后端实测留足(整数 ms,[1000, 2147483647];未设 = core 缺省;坏值拒启)。见 §9.4
|
|
112
|
+
AUTO_MODE_CLASSIFIER_TIMEOUT_MS=180000
|
|
91
113
|
```
|
|
92
114
|
- **全部默认关**(超时=0、断路器=false)→ 不设就和以前**逐字节一致**。
|
|
93
115
|
- 断路器**只在配了 `MODEL_GATEWAY_FALLBACK_URLS`(≥2 路)时才有意义**——它的价值是"开路即快速失败 → failover 立刻切备";单网关下它是 no-op(启动日志 `circuitBreakerNoop` 会提示)。只 `network/server/rate_limit` 计入连败,`auth`/`invalid_request` 不计(坏 key 熔断整网关无意义)。备用网关(最后一路)不套断路器。
|
|
@@ -386,6 +408,16 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
386
408
|
连接类异常惯于回声整条 DSN(`mysql://user:pass@host:3306/db`)。要读全文去**日志**:探针在
|
|
387
409
|
live→dead 翻转拍打 `store_probe_dead`,`error` 字段是原文。
|
|
388
410
|
钉:`test/health-store-live.test.ts` / `test/store-live-probe.test.ts`。
|
|
411
|
+
- 就绪位(**7.102.0+**,S-739):`/health` 与 `GET /readyz` 读**同一只**就绪判。就绪 ⇒ `/health` 不带 `ready`
|
|
412
|
+
键(正常形状不变)、`/readyz` 200 `{"ready":true}`;未就绪 ⇒ `/health` 带 `ready:false` + `readyReason`(首条原因
|
|
413
|
+
的人读句,名册未落那句逐字沿用旧句)+ `reasons`(闭集 `draining` / `model_roster_pending`,按优先序,与 `/readyz`
|
|
414
|
+
逐字同一份),`/readyz` 503 `{"ready":false,"reasons":[…]}` + `Retry-After: 5`(体只这两键,不带身份字段)。
|
|
415
|
+
⚠️ 7.102.0 起 **draining 也是未就绪**:此前 draining 形的 `/health` 只有 `draining:true`、不带 `ready` 键(读成
|
|
416
|
+
「就绪」),与同一刻计费提交的 503 自相矛盾。`storeLive:false` **不进**就绪判(见上一条:披露不代裁)。fleet 心跳的
|
|
417
|
+
`ready` 同一只判(名册未落时心跳 `ready:false`,落地当刻即再报一次)。fleet 心跳另带 **`restart`**(**7.102.0+**,S-741):
|
|
418
|
+
与 `/health.restart` 同一只取值口、逐字同一个对象(形 = settings-schema `RestartSignal`:`reasons` / `version` / `since`;
|
|
419
|
+
在场即「须重启才能应用配置」,缺席即不需要 —— 中心整行替换);重启信号出现 / 消失 / 变化的当刻即再报一次,不等下一拍心跳。
|
|
420
|
+
钉:`test/health-readyz.test.ts` / `test/health-readiness-single-source.test.ts`。
|
|
389
421
|
- 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
|
|
390
422
|
(1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
|
|
391
423
|
数据根下,清数据/隐私预期要按「sessions 在 engine-data 里」来做,不要按「只在内存」。
|
|
@@ -508,10 +540,24 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
|
|
|
508
540
|
**可选 — 接配置控制面(中心化模型/角色/团队配置)**
|
|
509
541
|
```bash
|
|
510
542
|
# 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 在场即 boot 拒启,只认 SEMA_REGISTRY_*
|
|
543
|
+
# 凭证两键名 SEMA_REGISTRY_TOKEN / SEMA_REGISTRY_TOKEN_FILE 与「二选一」判官的属主 = @sema-agent/settings-schema
|
|
544
|
+
# `CENTER_CREDENTIAL_ENV` / `judgeCenterCredentialDeclaration`(6.1.0 起;管理面 / 壳 / 本服务 import 同一处)
|
|
511
545
|
SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
|
|
512
546
|
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
|
|
513
547
|
# SEMA_REGISTRY_TOKEN_FILE=/path/to/token # 或:凭证文件(与上一行二选一,都设=拒启)。每条中心请求都重读 ⇒ 轮换 / 换账号改写文件即可,
|
|
514
|
-
# 中心请求面不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
|
|
548
|
+
# 中心请求面不重启;文件不可读 / 为空 = error 级 center_credential_unreadable,不当作无凭证。
|
|
549
|
+
# 内容两种类(S-698,7.102.0;判别 = settings-schema parseCenterCredentialFile):
|
|
550
|
+
# · 裸行 = 静态 bearer(worker 拉取令牌 / 用户 JWT),不续签,行为同 7.101.0;
|
|
551
|
+
# · JSON 信封 {"kind":"user-session",accessToken,refreshToken,expiresAt,issuer}(壳登录写)
|
|
552
|
+
# = 人的会话:引擎在到期前 CENTER_CREDENTIAL_RENEW_LEAD_MS 内自己续签并原子改写同一文件
|
|
553
|
+
# (续签只向与 SEMA_REGISTRY_URL 同源的 issuer 发;400 先重读文件再判,壳与引擎两写者不打架)。
|
|
554
|
+
# 🔴 拒启两形:内容读不成([config.center_credential_malformed]:`{` 开头坏 JSON / 信封多键缺键 /
|
|
555
|
+
# issuer 非 http(s) / 裸行不是令牌)、多租户(REQUIRE_PRINCIPAL=true)读到信封
|
|
556
|
+
# ([config.center_credential_user_session_multi_tenant])。运行期同形 = 该次取值 fail-closed + 一次 warn。
|
|
557
|
+
# CENTER_CREDENTIAL_RENEW_LEAD_MS=300000 # 续签提前量(ms;缺省 5 min)。0 = 关续签、只读信封(壳负责续);其余须为 [8000, 1800000] 内整数,
|
|
558
|
+
# 坏值拒启。续签成功 info center_credential_renewed;真失效(refresh token 已作废)warn
|
|
559
|
+
# center_credential_renew_failed 一次 + /health.centerCredential.renewal.state=failed,到期后中心请求
|
|
560
|
+
# 走各自既有的失败臂、不带过期令牌出站;issuer 不同源 warn center_credential_issuer_mismatch 一次、不发。
|
|
515
561
|
# ⚠️ 换账号(凭证身份变化)后,只在启动期物化的面 —— 技能 / MCP / A2A / 场景 —— 仍是上一个
|
|
516
562
|
# 身份的,直到重启(`/health` 报 restartRequired,reasons 含 skills);热面(models / roles /
|
|
517
563
|
# limits / 审批名单 …)在新身份第一份候选到达那一拍换代。「换账号即降档 + 当场拉取」候 S-667(不在 7.100.0)。
|
|
@@ -960,6 +1006,11 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s:成孤儿之后还要静多久
|
|
|
960
1006
|
生效值**为准:`GET /v1/config/catalog` 里 `DRAIN_GRACE_MS` 那一行的 `effectiveValue` 就是本进程真在用的
|
|
961
1007
|
数(它读的是活 config,不是这份文档里的默认值),启动期的夹取/警告行同源。本文档其余各处谈这条排空腿
|
|
962
1008
|
时都不带数字,数字只有这一处 —— 照抄默认值去算壳形预算会差一个数量级。
|
|
1009
|
+
- **排空期的第二发信号(`DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 `250`,7.102.0+)**:排空开始后再来一发 `SIGTERM` /
|
|
1010
|
+
`SIGINT`,只有与排空起点相隔 ≥ 这个毫秒数才算「别等了,硬停」;更近的是同一拍被投递了两遍(壳心跳误判、编排器
|
|
1011
|
+
重试),打一行 `drain_duplicate_signal_ignored`(带 `signal` / `sinceFirstMs` / `minGapMs`)后照常排空。真要立刻
|
|
1012
|
+
停:隔过这个间隔再发一次,或 `SIGKILL`;设 `0` 回到「每个第二发都硬停」的旧行为。合法域 `[0, 10000]` 整数毫秒
|
|
1013
|
+
(上界 = `DRAIN_GRACE_MS` 的地板),坏值拒启。不在排空时 `SIGINT` 仍是立即硬停。
|
|
963
1014
|
- **自退留痕 `<数据根>/exit-last.json`**:自退在 **drain 开始那一刻**就把判据写进数据根(`orphan` /
|
|
964
1015
|
`reason` / `idleForMs` / `attached` / `inflight` / `lingerMs` + `version` / `at` / `pid`)。壳的 exit
|
|
965
1016
|
watch 往往在流断之后才跑,只看退出码分不清"它自己退的"与"它崩了";读到这个文件就能把这个 pid 归入
|
|
@@ -999,7 +1050,7 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s(父没了之后的窗;见上一
|
|
|
999
1050
|
两键都缺席 ⇒ 整件不装配,零定时器。
|
|
1000
1051
|
- **自退判据 = 三合取,且必须连续满**「当前窗」(父还活着 = `ENGINE_LINGER_MS`;父没了 =
|
|
1001
1052
|
`ENGINE_ORPHAN_LINGER_MS`。判据一条,只有窗长两态):① 没有附着的 SSE 流;② 没有在飞的 run
|
|
1002
|
-
(与 `/health` 的 `inflight` **同一个读数**);③ 没有非探针 HTTP 触点(`/health`、`/metrics*` 不算 ——
|
|
1053
|
+
(与 `/health` 的 `inflight` **同一个读数**);③ 没有非探针 HTTP 触点(`/health`、`/readyz`、`/metrics*` 不算 ——
|
|
1003
1054
|
探针不是「有人在用」),**且**最后一条附着流断开也已满窗。任何一条不满足即把计时**清零**。
|
|
1004
1055
|
(最后那半句不是修辞:一条活得比 15s 拍频还短的流整个生命周期都落在两拍之间,自查腿从没看见过它 ——
|
|
1005
1056
|
只有在**关闭点**记一笔离场时刻,「已经没人附着满 N 秒」才是精确的。)
|
|
@@ -1140,14 +1191,14 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
|
|
|
1140
1191
|
**鉴权 / 身份(两个不同的头,按需):**
|
|
1141
1192
|
| 头 | 什么时候必须 | 含义 |
|
|
1142
1193
|
|---|---|---|
|
|
1143
|
-
| `Authorization: Bearer <SERVICE_AUTH_TOKEN>` | 设了 `SERVICE_AUTH_TOKEN`
|
|
1194
|
+
| `Authorization: Bearer <SERVICE_AUTH_TOKEN>` | 设了 `SERVICE_AUTH_TOKEN` 时,除 `/health` / `/readyz` 外的所有请求 | **谁有权调本服务**(OA 后端持有,服务到服务) |
|
|
1144
1195
|
| `x-agent-principal: user:42` | 设了 `REQUIRE_PRINCIPAL=true` 时 | **代表哪个终端用户**(决定 session 归属 + 记忆隔离;**绝不从 body 取**) |
|
|
1145
1196
|
|
|
1146
1197
|
| env 键 | 缺省 | 说明 |
|
|
1147
1198
|
|---|---|---|
|
|
1148
1199
|
| `PRINCIPAL_HEADER` | `x-agent-principal` | 上面那个身份头的**名字**。🔴 **缺省值是跨仓 wire 常量,不是给部署方自定义的**:这个名字**不在任何 wire 面上广播**(`/v1/capabilities`、`/health` 都没有它),所以没有下游能在运行期问 server「你叫它什么」——SDK 有一个 `principalHeader` 逃生舱但 cli/client-core 都没接线,浏览器端 BFF 是逐字硬编码,`mcp-oa` 侧车自己读同名 env(各读各的)。本旋钮只服务于「入口网关已经把身份写进别的头名」这一种特殊部署(让 server 迁就既有网关),不是给部署方起新名字用的。**改名即断下游**,但后果分四形(逐条对过代码):`REQUIRE_PRINCIPAL=true` 下提交腿(`POST /v1/tasks|/v1/runs`)是 `401` + **粗码 `auth.unauthorized`**(authorizer 抛的无 code HttpError 走状态码兜底映射),属主寻址的读/动词面是 `401 auth.principal_required`(路由级细码);两者的文案都逐字回显**你配的头名**,那是现场唯一能指认改名的线索。`REQUIRE_PRINCIPAL` 未开时更坏:**新会话照常成功**、每一条被当成**匿名**(session 归属 / 记忆隔离 / 规则车道 / operator 判定静默走无身份分支,无任何错误码),而**接续一条已属主的会话**会 `401`(`session is principal-owned; missing principal header …`)。⚠️ **SSO 与 direct-door 两条身份来源不经过这个头**(`verifiedPrincipal`),改名对它们无影响 —— 混合形部署因此会出现「一半调用方有身份、一半静默变匿名」的混着长。改了后果自负,且必须同批把每一家下游改到同名。取值须是**单个**合法 header 名(带空格/逗号 ⇒ 启动期拒启:它会撕裂 CORS `allow-headers`)。成文契约见 [`docs/ASSISTANT-WIRE-CONTRACT.md` §0.5](docs/ASSISTANT-WIRE-CONTRACT.md) |
|
|
1149
1200
|
|
|
1150
|
-
> ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`**
|
|
1201
|
+
> ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`** 等除 `/health` / `/readyz` 两只探针外的路由都要带 `Authorization: Bearer`——漏带一律 `401`。下面示例为简洁**省略了 Bearer**,真实调用请逐个补上。
|
|
1151
1202
|
|
|
1152
1203
|
**请求体字段(常用;受理**闭集**以 `docs/ASSISTANT-WIRE-CONTRACT.md` §1 为准 —— 本表不再重抄一份,S-710):**
|
|
1153
1204
|
| 字段 | 必填 | 说明 |
|
|
@@ -1286,6 +1337,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
1286
1337
|
| `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?,domainExists,managedNow,effectiveNow}}`;顶层带 `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 原文。 |
|
|
1287
1338
|
| `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。 |
|
|
1288
1339
|
| `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 **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
|
|
1340
|
+
| `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,「可能还在跑」)。`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 |
|
|
1341
|
+
| `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「探针」 |
|
|
1289
1342
|
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
1290
1343
|
|
|
1291
1344
|
> **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
|
|
@@ -1700,6 +1753,7 @@ armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类
|
|
|
1700
1753
|
| 旋钮 | 缺省 | 语义 |
|
|
1701
1754
|
|---|---|---|
|
|
1702
1755
|
| `PERMISSIONS_DISABLE_AUTO_MODE` | `false`(opt-in) | CC/center/settings 键名 `permissions.disableAutoMode` 的 **env 腿**(config catalog 登记 center 键名 `permissions.disableAutoMode`,settings-schema 现无 `permissions` 域 ⇒ 目录行 `domainExists:false`):**tighten-only** 棘轮 —— `true` ⇒ 本部署每个 principal 的 `caps.autoMode` 折成 `false`(center 授予 `true` **翻不回**);未设 ⇒ 不动 caps。布尔词表(`true/false/1/0/yes/no/on/off`),词表外的值**拒启**(安全轴旋钮不许静默回默认)。目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
|
|
1756
|
+
| `AUTO_MODE_CLASSIFIER_TIMEOUT_MS` | 未设(⇒ core 缺省) | 7.102.0 起(S-757):auto 分类器**一级梯子**的判决上限(整数 ms)→ core `RunnerDeps.autoMode.timeoutMs`。一级到点没裁 ⇒ 走下一级(`AUTO_MODE_CLASSIFIER_FALLBACK` 决定有没有下一级),走完仍没裁 ⇒ 这次门控调用**整条拒**、不出卡;最坏等待 ≈ 级数 × 本值 + 一次装配。**自托管慢网关**(排队 + prefill 以分钟计)在缺省上限内答不出 ⇒ auto 下工具一律被拒 —— 按后端实测留足(如 `180000`)。未设 / 空串 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游数字);合法域 `[1000, 2147483647]` 内的十进制整数,`0`(本座没有「关」)/ 亚秒 / 小数 / `1e5` 这类拼法 / 越 Node 定时器上界一律**拒启**。旋钮无条件(与 auto 是否武装无关);不影响 `permissionModeAuto` 读面。目录行见 `GET /v1/config/catalog`(approval 域) |
|
|
1703
1757
|
| `CROSS_SESSION_INBOUND` | 未设 | 跨终端会话设计线:CC settings 键名 `crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层,CC `policySettings` 对位)—— 本部署对**入站跨会话消息**的治理四态(词表取自引擎闭集,core 7.30.0 起):`wake` 投递且**宿主**可在收件箱增长时起一个 turn / `next-turn` 在下一个 turn 边界投递 / `hold` 停在收件会话的待审队列(模型看不见、不能据此行动)/ `refuse` 本部署整体退出这条车道。⚠️ 本服务宿主**不起 turn**,所以在本服务上 `wake` 按引擎契约的降级臂读作 `next-turn`。🔴 **旧词 `accept` 自 7.100.0(core 7.30.0 提货)起拒启** —— 它的原义就是 `next-turn`,改成那个词即可(零别名,拒启即通告)。🔴 **未设 ≠ `next-turn`**:未设 = 这一层不表态,由其余层与引擎的 mode-parity 判定决定。词表外的值**拒启**。⚠️ **生效前提**:引擎的 peer 目录席(`RunnerDeps.peerDirectory`)在场 —— 7.91.0 起已接线(会话枚举面 ∧ mailbox 店 ∧ 收件人生命周期面三合取),缺任一合取项的部署上本旋钮仍「就位待命」;目录行见 `GET /v1/config/catalog`(approval 域,security 轴) |
|
|
1704
1758
|
| `CROSS_SESSION_DIALOG_EXPIRY` | 未设(⇒ 引擎缺省 `5m`) | 同族:CC settings 键名 `dialogExpiry` 的部署层 —— 被 hold 的跨会话消息等待人审多久后按**安全缺省**结算(过期丢弃,**带回执**告知发送方,不静默吞)。词表 `60s`/`5m`/`10m`/`never`(`never` = 不设期限);未设 ⇒ 本仓**不铸键**,由引擎落它自己的缺省(不复制上游缺省值)。词表外拒启。⚠️ 生效前提同上。另注:CC 的同名键还有第二个消费面(转发到远端客户端的审批对话框停靠时长),本旋钮**今天不驱动**那一面 |
|
|
1705
1759
|
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -402,6 +402,33 @@ export declare function readParkRowTrueSeat(toolApproval: object, seat: ParkRowT
|
|
|
402
402
|
type ToolApprovalPark = Extract<CorePendingAction, {
|
|
403
403
|
kind: "tool_approval";
|
|
404
404
|
}>;
|
|
405
|
+
/**
|
|
406
|
+
* 🔴 **闭集词座的唯一窄读**(S-721 起 `mandate`;S-755 起 `origin`)—— 一座一只 core 成员筛,**名字是参数**(与
|
|
407
|
+
* {@link readParkRowTrueSeat} 同一条:一处解析,不是两只各写一遍的函数)。
|
|
408
|
+
* · `mandate`(core 7.32.0 #1092)—— 一问**站在哪一个 mandate 词上**(`mandated` 说「站在强制上」,本座说是 core
|
|
409
|
+
* `PERSISTED_RULE_MANDATES` 六词里的哪一个;core 只在强制位的**整因**是那一个词时铸 —— 治理 / hook / 祖先,或不止一个理由
|
|
410
|
+
* ⇒ 缺席:缺席从不命名一个词)。读者:活卡帧与活挂起行读 `AskRequest.mandate`(`tool-approval.ts`),运维队列行读寄存行
|
|
411
|
+
* 孪生座 `PendingAction.tool_approval.mandate`({@link parkRowFacts})。**不绑 `mandated`**(与 core `summarizeCheckpoint` 同一条筛法)。
|
|
412
|
+
* · `origin`(core 7.5.0 [ref] 的 park 孪生座;S-755)—— **谁提的这一问**,core `ASK_ORIGINS` 闭集一词。读者:运维队列行读寄存行
|
|
413
|
+
* 孪生座 `PendingAction.tool_approval.origin`({@link parkRowFacts})。活卡帧**不经**本函数:它读引擎在门上刚盖好的
|
|
414
|
+
* `AskRequest.origin`(盖章点即 core 的筛点),照旧只过「非空串」形门(`tool-approval.ts` 帧键 `origin` 顶注)。
|
|
415
|
+
*
|
|
416
|
+
* 什么时候经本函数 —— 一句话:**筛点在 core;server 只在「读的是持久层原字节、中间没有 core 投影」的地方亲手调 core 的那一只筛**。
|
|
417
|
+
* inbox 行读 core `CheckpointSummary.{mandate,origin}`(core `summarizeCheckpoint` 已用同一只 `isPersistedRuleMandate` / `isAskOrigin`
|
|
418
|
+
* 筛过)⇒ 那一面直投、不经本函数(与 `ruleStoreUnreadable` 同一条法理);运维队列行读的是 park 行 `pendingAction` 子树的原字节
|
|
419
|
+
* (两店 `listPending`,中间没有 core 投影)⇒ 经本函数 —— 于是 inbox 行与队列行对同一张 park 行(含手改行 / 世代差的集外词)逐字同答。
|
|
420
|
+
*
|
|
421
|
+
* **不抄词表**([ref]):闭集判定 = core 的成员筛({@link CLOSED_WORD_SEAT_GUARDS});值型 = core 寄存行上那一座的型。本仓对这些词
|
|
422
|
+
* **零分支**(echo-only:core 原话「What a surface does with a word is the surface's own rule」)⇒ 没有需要穷举的 switch;将来本仓若按
|
|
423
|
+
* 词分支,必须 `switch` 在 core 的值型上穷举。在场却读不出(集外词 / 非串值 —— 世代差、手改行)⇒ 按缺席处置 + 计一次
|
|
424
|
+
* `server.approval-card.closed-word-out-of-set`(S-249 族:缺席从不计数、读不出计一次;detail 前缀 = 座名,诊断经 {@link outOfSetDetail},
|
|
425
|
+
* 只读值不调用值)。
|
|
426
|
+
*/
|
|
427
|
+
export declare function readClosedWordSeat<S extends ClosedWordSeat>(carrier: object, seat: S): ClosedWordOf<S> | undefined;
|
|
428
|
+
/** 闭集词座的名:三个 core 载体上**同名**(`AskRequest` / 寄存行 `tool_approval` 臂 / `CheckpointSummary`)—— 任一处改名 / 退役 ⇒ 下面的围栏编译红。 */
|
|
429
|
+
type ClosedWordSeat = "mandate" | "origin";
|
|
430
|
+
/** 一座的值型 = core 寄存行上那一座的型(去掉可选位的 `undefined`)。 */
|
|
431
|
+
type ClosedWordOf<S extends ClosedWordSeat> = NonNullable<ToolApprovalPark[S]>;
|
|
405
432
|
/**
|
|
406
433
|
* 🔴 S-700 P1(client-core CC-180 / cli L-652 的根子;板 [ref] 订正 → [ref] 立案)—— **park 行事实**:
|
|
407
434
|
* 运维队列行(`GET /v1/approvals` + `/stream`)顶层展开的那几位。型**从 core 寄存行派生,不手抄**(S-249 同一条
|
|
@@ -415,8 +442,12 @@ type ToolApprovalPark = Extract<CorePendingAction, {
|
|
|
415
442
|
* 的 `FACT_KEYS` 对本型双向围栏,少列一名同样编译红。
|
|
416
443
|
* core d.ts 说三位彼此独立:`requiresRealApproval`(只有人能清 —— 蕴含 `mandated`)、`mandated`(没有规则能清)、
|
|
417
444
|
* `ruleOffersAbsence`(报价车道有什么可给;「never a substitute for this one」)—— 投影逐位照投,不互推。
|
|
445
|
+
* S-721(core 7.32.0 #1092)第四位 `mandate`:强制位站在哪一个词上(经 {@link readClosedWordSeat};词缺席 ≠ 不强制,不从 `mandated` 推)。
|
|
446
|
+
* S-755(cli L-695 / [ref],client-core [ref])第五位 `origin`:谁提的这一问(core `ASK_ORIGINS` 闭集一词,经 {@link readClosedWordSeat};
|
|
447
|
+
* 与活卡帧 `origin` / inbox 行 `origin` 同名同义同值)。此前只有活卡帧与 inbox 行带它 ⇒ 耐久重开 / 重连时壳从队列行读不到出身词,
|
|
448
|
+
* bypass 姿态下按 `ask_rule` / `hook` 照问的那条壳规则无从落地。缺席 = 老行(core 7.5.0 之前 park 的)/ 集外词(计 S-249 tag)。
|
|
418
449
|
*/
|
|
419
|
-
export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated">;
|
|
450
|
+
export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "ruleOffersAbsence" | "mandated" | "mandate" | "origin">;
|
|
420
451
|
/**
|
|
421
452
|
* S-700 —— park 行事实的**唯一投影**(`pendingAction` 是寄存行上 core 铸的那一只,读自持久层 ⇒ `unknown`)。
|
|
422
453
|
*
|
|
@@ -434,7 +465,9 @@ export type ParkRowFacts = Pick<ToolApprovalPark, "requiresRealApproval" | "rule
|
|
|
434
465
|
* 同答(`test/park-row-facts.test.ts` 钉)。不计数:inbox 那一面对同一形同样静默缺席,两面同律;
|
|
435
466
|
* · `ruleOffersAbsence` —— 经 {@link readRuleOffersAbsence}(活卡帧 / `card_json` 同一只 schema、同一个集外 tag):
|
|
436
467
|
* park 孪生座与同步座是 core 同一个 factory 铸的同一闭集,不另起第二份词表;
|
|
437
|
-
* ·
|
|
468
|
+
* · `mandate` / `origin` —— 经 {@link readClosedWordSeat}(一座一只 core 成员筛 `isPersistedRuleMandate` / `isAskOrigin`,与 core
|
|
469
|
+
* `summarizeCheckpoint` 投 inbox 行时是同一只筛 ⇒ 两条耐久读面对同一张行同答;集外词读作缺席 + 计同一个集外 tag);
|
|
470
|
+
* · 缺席 ≠ false:各位都只在「真 / 有词」时在场,消费端读**在场**。
|
|
438
471
|
*
|
|
439
472
|
* 🔴 **这是展示/分诊投影,不是权限判据**(core 对两位的定性都是 display metadata:「the resume belts keep reading
|
|
440
473
|
* the gate's own bit」)。本投影对「在场却读不出」的方向是**缺席**(卡上少一行 ≪ 卡上出现生词);一条要从
|
package/dist/approval-card.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX } from "@sema-agent/core";
|
|
2
|
+
import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskOrigin, isPersistedRuleMandate } from "@sema-agent/core";
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
5
|
import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
|
|
@@ -270,13 +270,31 @@ export function readParkRowTrueSeat(toolApproval, seat) {
|
|
|
270
270
|
const v = seat in toolApproval ? toolApproval[seat] : undefined;
|
|
271
271
|
return v === true;
|
|
272
272
|
}
|
|
273
|
+
export function readClosedWordSeat(carrier, seat) {
|
|
274
|
+
const raw = seat in carrier ? carrier[seat] : undefined;
|
|
275
|
+
if (raw === undefined)
|
|
276
|
+
return undefined;
|
|
277
|
+
const isMember = CLOSED_WORD_SEAT_GUARDS[seat];
|
|
278
|
+
if (isMember(raw))
|
|
279
|
+
return raw;
|
|
280
|
+
recordFailOpen("server.approval-card.closed-word-out-of-set", `${seat}=${outOfSetDetail(raw)}`);
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const CLOSED_WORD_SEAT_GUARDS = {
|
|
284
|
+
mandate: isPersistedRuleMandate,
|
|
285
|
+
origin: isAskOrigin,
|
|
286
|
+
};
|
|
273
287
|
export function parkRowFacts(pendingAction) {
|
|
274
288
|
if (typeof pendingAction !== "object" || pendingAction === null || !("kind" in pendingAction) || pendingAction.kind !== "tool_approval")
|
|
275
289
|
return {};
|
|
276
290
|
const ruleOffersAbsence = readRuleOffersAbsence(pendingAction);
|
|
291
|
+
const mandate = readClosedWordSeat(pendingAction, "mandate");
|
|
292
|
+
const origin = readClosedWordSeat(pendingAction, "origin");
|
|
277
293
|
return {
|
|
278
294
|
...(readParkRowTrueSeat(pendingAction, "requiresRealApproval") ? { requiresRealApproval: true } : {}),
|
|
279
295
|
...(readParkRowTrueSeat(pendingAction, "mandated") ? { mandated: true } : {}),
|
|
296
|
+
...(mandate !== undefined ? { mandate } : {}),
|
|
297
|
+
...(origin !== undefined ? { origin } : {}),
|
|
280
298
|
...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
|
|
281
299
|
};
|
|
282
300
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type BackgroundShellHardKillReceipt } from "@sema-agent/core";
|
|
2
|
+
import type { Logger } from "./observability/logger.js";
|
|
3
|
+
/** 受据四词的计数(日志汇总行与调用方读数共用)。 */
|
|
4
|
+
export interface HardKillTally {
|
|
5
|
+
killed: number;
|
|
6
|
+
alreadyGone: number;
|
|
7
|
+
retainedSkipped: number;
|
|
8
|
+
/** 没有同步硬收落地(普通 kill 已派)—— 「未确认」,不是失败。 */
|
|
9
|
+
unconfirmed: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 受据的**唯一读者**:逐行一条 `background_shell_hard_kill`(`{ taskId, type, outcome, reason, owner?, retained? }` —— 不带
|
|
13
|
+
* pid / shellId:core 契约「pid 不出 env」,受据上本来就没有),返回四词计数。本函数自己不抛;注入形 logger 若抛,异常**原样上抛**
|
|
14
|
+
* 给调用方(kill 已在调用前落地,受据不丢):退出边界与 E21 级联各自在外层 try 里 warn / `console.error`(生产 sink 不抛,
|
|
15
|
+
* `observability/logger.ts` 的 `emit` 已包),停止口则让它走全局 500 —— 不在这里写静默的空 catch([ref] 棘轮)。
|
|
16
|
+
* `outcome` 按 core 闭集穷举分派:集外词 = 编译红(`never` 臂),不是运行期的静默默认。
|
|
17
|
+
*/
|
|
18
|
+
export declare function reportHardKillReceipts(logger: Pick<Logger, "info"> | undefined, receipts: readonly BackgroundShellHardKillReceipt[], reason: string): HardKillTally;
|
|
19
|
+
/** {@link reapSessionBackgroundHard} 的读数。 */
|
|
20
|
+
export interface SessionHardReap {
|
|
21
|
+
/** 软面①(core 会话锚 `reapSessionBackground`)直接收到的行数(core 口径:不含受委托子代连带收掉的孙行)。 */
|
|
22
|
+
reaped: number;
|
|
23
|
+
/** 软面②(TaskStop 面)发出的行数:硬面将收、而会话锚没收的仍在跑的 shell 行(`includeRetained:true` 时的 retain 行 / 本会话 run 的
|
|
24
|
+
* 非会话驻留行)。 */
|
|
25
|
+
softStopped: number;
|
|
26
|
+
/** 硬面逐行受据(只有 shell 行:`background_bash` / `monitor`)。 */
|
|
27
|
+
receipts: BackgroundShellHardKillReceipt[];
|
|
28
|
+
tally: HardKillTally;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* **会话级收敛 —— 一只函数,「软 → 宽限 → 硬」**(车OL O1 / O2):
|
|
32
|
+
* ① 软面:core 会话锚 `reapSessionBackground(sessionId, scope)`(本会话会话驻留、非 retain 的行走 env 梯子;同时结算本会话的后台
|
|
33
|
+
* agent / workflow),再 {@link softStopRemaining}(硬面将收、会话锚没收的仍在跑的 shell 行,逐行 TaskStop 软面);
|
|
34
|
+
* ② 宽限:{@link awaitSoftStopGraces} —— 硬面不早于它将重访的任何一行的「软停时刻 + `HOST_BG_KILL_GRACE_MS`」(host 梯子的属主常量,
|
|
35
|
+
* `plugins/remote-env-host.ts` 导出,不另抄;不分车道)。本次刚软停的行 ⇒ 等满一次宽限;几十毫秒前被另一次收敛 / 模型 TaskStop 软停
|
|
36
|
+
* 的行 ⇒ 等它剩下的那段;都不在宽限内 ⇒ 不等。同一会话两次重叠收敛(壳先打停止口、别处再 DELETE)因此都不早于前者软停 + 宽限,
|
|
37
|
+
* 不需要按会话单飞;
|
|
38
|
+
* ③ 硬面:`killAllBackgroundShellsNow({ sessionId, scope, includeRetained, reason })`(会话过滤 = 会话锚自己的键:本会话的会话驻留行
|
|
39
|
+
* + run 的 task id 就是会话 id 的 run 行;同一个 id 在别的 scope 是别的会话;别的会话的行永远不碰)—— 宽限里跑完处理器的进程答
|
|
40
|
+
* `already_gone`,屏蔽 / 没跑完的在这一枪组 SIGKILL(与 host 梯子自己到点的那一枪同拍,两枪都是组 SIGKILL,幂等)。
|
|
41
|
+
*
|
|
42
|
+
* `scope` = 会话行属主 `owner ?? "default"`(与 core 注册铸值 `spec.principal ?? "default"` 同源 —— 缺席 scope 在 core 的
|
|
43
|
+
* `canAccess` 上 fail-closed、一行都收不到,[ref] 钉过这一病)。
|
|
44
|
+
*
|
|
45
|
+
* 两处调用(见文件头 ①),都 `await`:E21 删除级联 `includeRetained:false`(显式活服务照留);停止口照传请求体。应答在硬面之后发,
|
|
46
|
+
* 代价 = 有 shell 行刚被软停的会话,DELETE / 停止口的应答慢到最后一行的「软停 + 宽限」(通常一次宽限;操作员旋钮 `HOST_BG_KILL_GRACE_MS`,缺省 1000;要几乎不等写 `1` ——
|
|
47
|
+
* `numEnvOr` 把 `0` 读作缺席、回落缺省,与它供的其余执行车道旋钮同一条语义)。
|
|
48
|
+
* 受委托帮手的 shell 挂在帮手自己的会话上,本函数碰不到(core KL-1107a / #1109,候 core)。
|
|
49
|
+
* 注册表是**进程内**的:只收本副本上的行(多副本部署里别的副本上的行由那个副本自己的出口收)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function reapSessionBackgroundHard(sessionId: string, scope: string, opts: {
|
|
52
|
+
includeRetained: boolean;
|
|
53
|
+
reason: string;
|
|
54
|
+
}, logger: Pick<Logger, "info" | "warn"> | undefined): Promise<SessionHardReap>;
|
|
55
|
+
//# sourceMappingURL=background-session-reap.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
3
|
+
import { HOST_BG_KILL_GRACE_MS } from "./plugins/remote-env-host.js";
|
|
4
|
+
export function reportHardKillReceipts(logger, receipts, reason) {
|
|
5
|
+
const tally = { killed: 0, alreadyGone: 0, retainedSkipped: 0, unconfirmed: 0 };
|
|
6
|
+
for (const r of receipts) {
|
|
7
|
+
switch (r.outcome) {
|
|
8
|
+
case "killed":
|
|
9
|
+
tally.killed++;
|
|
10
|
+
break;
|
|
11
|
+
case "already_gone":
|
|
12
|
+
tally.alreadyGone++;
|
|
13
|
+
break;
|
|
14
|
+
case "retained_skipped":
|
|
15
|
+
tally.retainedSkipped++;
|
|
16
|
+
break;
|
|
17
|
+
case "no_pgid":
|
|
18
|
+
tally.unconfirmed++;
|
|
19
|
+
break;
|
|
20
|
+
default: {
|
|
21
|
+
const unreachable = r.outcome;
|
|
22
|
+
return unreachable;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
logger?.info("background_shell_hard_kill", {
|
|
26
|
+
taskId: r.taskId,
|
|
27
|
+
type: r.type,
|
|
28
|
+
outcome: r.outcome,
|
|
29
|
+
reason,
|
|
30
|
+
...(r.owner !== undefined ? { owner: r.owner } : {}),
|
|
31
|
+
...(r.retained === true ? { retained: true } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return tally;
|
|
35
|
+
}
|
|
36
|
+
const SOFT_STOP_LIST_LIMIT = 500;
|
|
37
|
+
const isShellRow = (type) => type === "background_bash" || type === "monitor";
|
|
38
|
+
function softStopRemaining(access, includeRetained, logger) {
|
|
39
|
+
const rows = defaultTaskRegistry
|
|
40
|
+
.list(access, { limit: SOFT_STOP_LIST_LIMIT })
|
|
41
|
+
.filter((r) => r.status === "running" && isShellRow(r.type) && (r.retained !== true || includeRetained));
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
const failed = (fields) => logger?.warn("session_background_soft_stop_failed", { taskId: r.task_id, type: r.type, ...fields, note: "the hard face right after the grace still stops this row" });
|
|
44
|
+
defaultTaskRegistry.markStopSource(r.task_id, "system");
|
|
45
|
+
void defaultTaskRegistry.stopTask(r.task_id, access).then((res) => {
|
|
46
|
+
if (res.isError === true)
|
|
47
|
+
failed({ error: res.details.code ?? res.details.error ?? "stop_failed" });
|
|
48
|
+
}, (err) => failed({ error: err instanceof Error ? err.name : "stop threw" }));
|
|
49
|
+
}
|
|
50
|
+
return rows.map((r) => r.task_id);
|
|
51
|
+
}
|
|
52
|
+
async function awaitSoftStopGraces(access, softStop) {
|
|
53
|
+
const honored = new Set();
|
|
54
|
+
let notBefore = 0;
|
|
55
|
+
const honor = (taskId, softStoppedAt) => {
|
|
56
|
+
if (honored.has(taskId))
|
|
57
|
+
return;
|
|
58
|
+
honored.add(taskId);
|
|
59
|
+
notBefore = Math.max(notBefore, softStoppedAt + HOST_BG_KILL_GRACE_MS);
|
|
60
|
+
};
|
|
61
|
+
for (const id of softStop.stoppedNow)
|
|
62
|
+
honor(id, softStop.at);
|
|
63
|
+
for (;;) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const rows = defaultTaskRegistry.list(access, { limit: SOFT_STOP_LIST_LIMIT });
|
|
66
|
+
for (const r of rows) {
|
|
67
|
+
if (r.status === "killed" && isShellRow(r.type))
|
|
68
|
+
honor(r.task_id, Math.min(r.updatedAt, now));
|
|
69
|
+
}
|
|
70
|
+
if (softStop.anchorReaped > 0 && rows.length >= SOFT_STOP_LIST_LIMIT)
|
|
71
|
+
notBefore = Math.max(notBefore, softStop.at + HOST_BG_KILL_GRACE_MS);
|
|
72
|
+
if (notBefore <= now)
|
|
73
|
+
return;
|
|
74
|
+
await delay(notBefore - now);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function reapSessionBackgroundHard(sessionId, scope, opts, logger) {
|
|
78
|
+
const access = { owner: sessionId, sessionId, scope };
|
|
79
|
+
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
|
|
80
|
+
const stoppedNow = softStopRemaining(access, opts.includeRetained, logger);
|
|
81
|
+
const at = Date.now();
|
|
82
|
+
await awaitSoftStopGraces(access, { at, stoppedNow, anchorReaped: reaped });
|
|
83
|
+
const softStopped = stoppedNow.length;
|
|
84
|
+
const receipts = defaultTaskRegistry.killAllBackgroundShellsNow({ sessionId, scope, includeRetained: opts.includeRetained, reason: opts.reason });
|
|
85
|
+
const tally = reportHardKillReceipts(logger, receipts, opts.reason);
|
|
86
|
+
if (reaped > 0 || softStopped > 0 || receipts.length > 0)
|
|
87
|
+
logger?.info("session_background_reaped", { sessionId, reaped, softStopped, reason: opts.reason, includeRetained: opts.includeRetained, ...tally });
|
|
88
|
+
return { reaped, softStopped, receipts, tally };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=background-session-reap.js.map
|
|
@@ -4,12 +4,13 @@ import { type PromptsDomainFaces } from "../prompts-domain-validate.js";
|
|
|
4
4
|
import { type Scenario, type ScenarioDeps, type ScenarioDetail } from "../capabilities/scenarios.js";
|
|
5
5
|
import type { LoadedSkill } from "../capabilities/skills.js";
|
|
6
6
|
import { type ServiceConfig } from "../config.js";
|
|
7
|
+
import { type CenterCredentialHealthView } from "../center-credential-renewer.js";
|
|
7
8
|
import { type ModelKeyRef } from "../key-resolver.js";
|
|
8
9
|
import type { Logger } from "../observability/logger.js";
|
|
9
10
|
import type { Metrics } from "../observability/metrics.js";
|
|
10
11
|
import type { UnpricedCeilingVerdict } from "./unpriced-ceiling-verdict.js";
|
|
11
12
|
import type { ConsolidationSeatWatch } from "./memory-consolidation.js";
|
|
12
|
-
import { type
|
|
13
|
+
import { type EngineRestartSignal } from "../config-center/facade.js";
|
|
13
14
|
import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
|
|
14
15
|
import type { LimitSync } from "./limit-sync.js";
|
|
15
16
|
export interface ConfigCenterRuntimeCtx {
|
|
@@ -27,6 +28,8 @@ export interface CenterScenarioOverlayTargets {
|
|
|
27
28
|
/** overlay 前捕获的六内建名(centerScenarioDetails 的 shadow 判据)。 */
|
|
28
29
|
builtinScenarioNames: string[];
|
|
29
30
|
}
|
|
31
|
+
/** S-740 / S-741:config-center 持有的两样自报状态(fleet 心跳读它们)—— 闭集:加一样 = 写口与订阅方一起表态。 */
|
|
32
|
+
export type SelfReportChange = "roster_landed" | "restart_signal";
|
|
30
33
|
/** 跨段可变状态的**唯一**对外读口 + 三个原位动作。字段语义见各方法上的注释。 */
|
|
31
34
|
export interface ConfigCenterRuntime {
|
|
32
35
|
/** 配置源形态;`undefined` = 纯 env 部署(本运行时的所有分支不存在)。 */
|
|
@@ -64,8 +67,10 @@ export interface ConfigCenterRuntime {
|
|
|
64
67
|
consolidationSeat?: ConsolidationSeatWatch;
|
|
65
68
|
checkpointStorePresent: boolean;
|
|
66
69
|
}): void;
|
|
67
|
-
/** [ref]-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。 */
|
|
70
|
+
/** [ref]-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。S-698:中心凭证续签器的定时器同停(同一条收尾链)。 */
|
|
68
71
|
stopRefreshLoop(): void;
|
|
72
|
+
/** S-698:`/health.centerCredential` 的取值口 —— 凭证来源的健康态 + 续签器状态;都健康 / 没有中心 ⇒ `undefined`(键缺席)。 */
|
|
73
|
+
centerCredentialState(): CenterCredentialHealthView | undefined;
|
|
69
74
|
/** [ref](core 5.50 [ref]):mid-turn MCP 撤销台账读口,喂 `RunnerDeps.mcpRevocations` 席。
|
|
70
75
|
* **取走即声明接线**——调用本方法后,restart 信号的 mcp 片对纯删除向豁免(台账即时承接);
|
|
71
76
|
* 不取 ⇒ 旧 restart 语义逐字保留(fail-closed)。 */
|
|
@@ -83,8 +88,15 @@ export interface ConfigCenterRuntime {
|
|
|
83
88
|
/** boot ready 门:布防(`CONFIG_REQUIRE_ROSTER=true`,远程 registry 非 dryRun)且 roster 未落地 =
|
|
84
89
|
* 计费提交 503 + /health ready:false。缺省未布防 ⇒ 恒 true。 */
|
|
85
90
|
modelReady(): boolean;
|
|
86
|
-
/** /
|
|
87
|
-
|
|
91
|
+
/** S-740 / S-741:本副本**自报状态**里由 config-center 持有的那两样 —— roster 落地(`modelReady` 翻真,one-way latch)与
|
|
92
|
+
* restart 信号(`restartState()` 出现 / 消失 / 变)—— 任一翻转的那一拍通知订阅者。装配层接 fleet 心跳的 `announceNow`,
|
|
93
|
+
* 让中心在翻转当刻就知道本副本可路由 / 要重启(不等下一拍心跳;draining 那一样由收尾链直调 `announceNow`,同一个触发)。
|
|
94
|
+
* 在状态**写定之后**调用,同步、逐个,入参 = 这一次翻的是哪一样(闭集 {@link SelfReportChange});不回放(订阅方在订阅前已读过
|
|
95
|
+
* 当刻值)。同一拍两样都翻 ⇒ 通知两次(心跳侧 `announceNow` 单飞合并,见 fleet-client)。订阅者抛错只 warn
|
|
96
|
+
* (`self_report_listener_failed`),不打断配置拍、不吞后一个订阅者。 */
|
|
97
|
+
onSelfReportChange(listener: (changed: SelfReportChange) => void): void;
|
|
98
|
+
/** /health.restart 与 fleet 心跳 `restart` 的**同一只**取值口(orchestrator 自动重启信号;settings-schema `RestartSignal` 形)。 */
|
|
99
|
+
restartState(): EngineRestartSignal | undefined;
|
|
88
100
|
/** /health.modelPlaneDeferred:被扣着、还没有 next-boot handoff 的候选(codex R18 起的状态机)。S-668 波 0 起
|
|
89
101
|
* 模型面不再 defer,键名保留,今天承载的是 durable LKG 发布债与 skills 债(见 `planeDeferredNoHandoff`)。 */
|
|
90
102
|
planeDeferredState(): {
|