@sema-agent/server 7.56.0 → 7.57.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/README.md +3 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +101 -9
- package/dist/approval-ask-audit-store.d.ts +100 -1
- package/dist/approval-ask-audit-store.js +103 -2
- package/dist/approval-card.d.ts +148 -13
- package/dist/approval-card.js +82 -13
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +18 -0
- package/dist/auto-mode-face.d.ts +111 -0
- package/dist/auto-mode-face.js +99 -0
- package/dist/bench/s1/arms.js +5 -5
- package/dist/bench/s1/live-deps.js +11 -11
- package/dist/bench/s1/run-firm.js +3 -0
- package/dist/bench/s1/runner-ctx.d.ts +4 -0
- package/dist/bench/s1/runner-ctx.js +7 -0
- package/dist/boot/config-center.js +23 -1
- package/dist/boot/coordinators.js +13 -1
- package/dist/boot/parked-revive-gate.d.ts +7 -2
- package/dist/boot/parked-revive-gate.js +12 -1
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/runner-deps.d.ts +12 -0
- package/dist/boot/runner-deps.js +28 -3
- package/dist/boot/runtime-caps.d.ts +19 -9
- package/dist/boot/runtime-caps.js +49 -21
- package/dist/boot/session-shell-gate-registry.d.ts +39 -0
- package/dist/boot/session-shell-gate-registry.js +21 -0
- package/dist/config-catalog.js +14 -5
- package/dist/config-types.d.ts +41 -10
- package/dist/config.d.ts +21 -0
- package/dist/config.js +40 -5
- package/dist/http/route-ctx.d.ts +75 -0
- package/dist/http/routes/a2a-serve.js +5 -3
- package/dist/http/routes/approvals-assistant.js +19 -6
- package/dist/http/routes/capabilities.js +21 -4
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/http/routes/rules.js +3 -2
- package/dist/http/routes/runs.d.ts +0 -14
- package/dist/http/routes/runs.js +20 -8
- package/dist/http/routes/tasks.js +34 -8
- package/dist/http/routes/workflows.js +19 -4
- package/dist/http/server.d.ts +11 -1
- package/dist/http/server.js +215 -43
- package/dist/http/wire-types.d.ts +9 -4
- package/dist/leader/diffout.js +2 -1
- package/dist/leader/endpoint.js +45 -12
- package/dist/leader/fanout.js +6 -5
- package/dist/leader/leader.js +17 -14
- package/dist/leader/merge.js +17 -12
- package/dist/leader/planner.js +3 -2
- package/dist/leader/repair-oracle.js +6 -4
- package/dist/leader/repair-wire.js +5 -3
- package/dist/leader/wire.d.ts +30 -1
- package/dist/leader/wire.js +36 -19
- package/dist/main.js +43 -7
- package/dist/observability/err-text.d.ts +6 -0
- package/dist/observability/err-text.js +10 -0
- package/dist/observability/fail-open.d.ts +40 -0
- package/dist/observability/fail-open.js +19 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +120 -0
- package/dist/observability/run-terminal-log.js +360 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
- package/dist/orchestration/workflow-completion-inbox.js +8 -0
- package/dist/permission-rule-vocab.d.ts +40 -0
- package/dist/permission-rule-vocab.js +17 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
- package/dist/plugins/checkpoint-store-sql.js +19 -3
- package/dist/plugins/file-run-store.d.ts +3 -34
- package/dist/plugins/file-run-store.js +19 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +3 -0
- package/dist/plugins/memory-run-store.d.ts +3 -15
- package/dist/plugins/memory-run-store.js +19 -0
- package/dist/plugins/permission-rule-store-file.js +8 -3
- package/dist/plugins/permission-rule-store-sql.js +16 -10
- package/dist/plugins/remote-scratchpad.js +3 -2
- package/dist/plugins/run-store-sql.d.ts +3 -42
- package/dist/plugins/run-store-sql.js +30 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-contracts.d.ts +66 -1
- package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
- package/dist/plugins/workflow-run-store-sql.js +10 -2
- package/dist/rules-consent.js +20 -13
- package/dist/run-cancel-context.d.ts +13 -0
- package/dist/run-cancel-context.js +15 -0
- package/dist/run-local.js +2 -2
- package/dist/runs.d.ts +4 -1
- package/dist/runs.js +41 -10
- package/dist/runtime-caps-resolver.d.ts +133 -17
- package/dist/runtime-caps-resolver.js +41 -3
- package/dist/task-settings.d.ts +57 -3
- package/dist/task-settings.js +78 -5
- package/dist/task-workflow.d.ts +32 -2
- package/dist/task-workflow.js +8 -3
- package/dist/tool-approval.d.ts +26 -3
- package/dist/tool-approval.js +96 -13
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +13 -4
- package/dist/trace/ledger-sink.js +14 -6
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +10 -1
- package/dist/trace/redact.d.ts +22 -11
- package/dist/trace/redact.js +655 -20
- package/dist/trace/sema-provenance.d.ts +26 -0
- package/dist/trace/sema-provenance.js +11 -0
- package/dist/turn-activity.d.ts +32 -2
- package/dist/turn-activity.js +29 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -151,8 +151,8 @@ The server is configured entirely through environment variables. The most import
|
|
|
151
151
|
| `MODEL_API_KEY` | — | Gateway API key (optional) |
|
|
152
152
|
| `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
|
|
153
153
|
| `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory`. (`TIDB_HOST`/`_PORT`/`_USER`/`_PASSWORD`/`_DATABASE`/`_POOL_SIZE` are retired names, not an alias — since 5.0.0 setting any of them refuses to boot, pointing at the `MYSQL_*` replacement; TiDB itself connects fine through `MYSQL_*`, since it speaks the MySQL protocol.) (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
|
|
154
|
-
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql` (durable session center) / `auto`. *Defaults to durable when `DB_BACKEND` is explicitly `mysql`/`pg` |
|
|
155
|
-
| `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (fail-closed) |
|
|
154
|
+
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql` (durable session center) / `auto`. *Defaults to durable when `DB_BACKEND` is explicitly `mysql`/`pg`. (`tidb` is a RETIRED public name — setting it refuses to boot; since 7.57.0 `GET /v1/config/catalog` also echoes the public word `mysql` instead of the internal `tidb` label) |
|
|
155
|
+
| `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (fail-closed). Under `CONFIG_PROVIDER=local` a `config.d/remote-exec.json` **wins over** this env (deliberate: the file is the single-machine source of truth). Since 7.57.0 that preemption is **loud** — `remote_exec_lane_preempted_by_file` names the preempted lane, the effective one, and whether it is an **isolation downgrade** (an isolated lane replaced by a non-isolated one); a file naming a not-yet-wired isolated lane (`e2b`/`k8s`/`ssh`/`adb`) leaves the env lane in force and says so |
|
|
156
156
|
| `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | unset | **SSH lane host-key verification** (`REMOTE_EXEC=ssh` only). `SSH_HOST_FINGERPRINT` = the host key's SHA256 base64 fingerprint (`SHA256:` prefix and `=` padding both optional); `SSH_KNOWN_HOSTS` = a path to a `known_hosts` file, matched literally on `host` / `[host]:port`. **Either one present ⇒ strict verification, a mismatch REFUSES the connection** (the error carries both fingerprints plus an `ssh-keyscan` line); both set ⇒ both must pass. **Both unset ⇒ a loud per-connection warning (`ssh_host_key_unverified`, man-in-the-middle risk) and the connection proceeds** — the deliberate posture for the batch-deploy lane, registered as a P-DEBT fail-open (`docs/FAIL-OPEN-CENSUS.md`). A malformed fingerprint or an unreadable `known_hosts` path **refuses to start**. Not supported in `known_hosts`, on purpose: hashed (`\|1\|…`) lines, wildcard patterns, `@cert-authority` — all skipped, so a host whose only entries are those is REFUSED (`@revoked` is honored and wins over any plain line, whatever the order) |
|
|
157
157
|
| `HOOKS_TIMEOUT_MS` | unset (engine default 600000) | Per-invocation time bound for every hook seat (core `Hooks.timeoutMs`). Unset = the engine's own default (this server does not copy upstream defaults). `0` is honored as written (every seat expires immediately). A non-integer, negative, or above-`setTimeout`-ceiling (2147483647) value **refuses to start** |
|
|
158
158
|
| `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane) |
|
|
@@ -160,6 +160,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
160
160
|
| `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
|
|
161
161
|
| `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
|
|
162
162
|
| `MANUAL_MODE_SHELL_GATE` | unset | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence). **Unset is not "off"**: since 7.12.0 the caller's explicit `permissionMode` supplies the baseline this knob tightens from (`bypassPermissions` → `off`, `auto`/`default`/`acceptEdits`/`plan` → `classify`; **no** mode stated → no gate, core's `off` default). This knob only ever raises that baseline — it has no relax half, so `off` is accepted as an explicit **no-op** (a boot line says so; not symmetric with `SENSITIVE_WRITE_PATTERNS=off`, which really does clear a set). **Any other value refuses to start** (7.12.0, BREAKING for a deployment that had a typo: it was previously treated as unset, i.e. silently no gate at all) |
|
|
163
|
+
| `PERMISSIONS_DISABLE_AUTO_MODE` | `false` | Local mirror of CC `permissions.disableAutoMode` — the **org deny** bit for `permissionMode:"auto"` (paired with core's intent-arming rule). **Tighten-only**: `true` folds every principal's `runtimeCaps.autoMode` to `false` (a center grant cannot flip it back); unset leaves caps untouched, so on a center-less box a shell asking for `auto` **arms** the classifier once the paired core (intent-arming rule "requested ∧ classifier seat ∧ `autoMode !== false`" — absence is not a deny) is installed; on core 7.2.0 the engine still uses the old "org grant" rule (`permissionModeAuto.intentArming:false`), so the self-check answers `armed:true` only for a center-granted principal and `deployment_incapable` on a center-less box. Boolean word table; any other value refuses to start. Self-check: `GET /v1/capabilities?permissionMode=auto` → `permissionModeAuto.{armed, reason, model}` (USAGE §9.4) |
|
|
163
164
|
| `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
|
|
164
165
|
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
|
|
165
166
|
| `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `120000` | First-token timeout |
|
package/README.zh-CN.md
CHANGED
|
@@ -140,8 +140,8 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
|
|
|
140
140
|
| `MODEL_API_KEY` | — | 网关 key(可选) |
|
|
141
141
|
| `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
|
|
142
142
|
| `DB_BACKEND` | `local`* | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB —— TiDB 走这个值,**没有** `tidb` 别名,写它启动即拒)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)/ `memory`(显式纯内存)。显式设置 `mysql`/`pg` 时 session 自动转 durable。*单租户裸 boot 缺省 `local`;`REQUIRE_PRINCIPAL=true` 的多租户裸 boot 缺省 `memory`(local 与多租户互斥) |
|
|
143
|
-
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable |
|
|
144
|
-
| `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(fail-closed) |
|
|
143
|
+
| `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable。(`tidb` 是**退役公名**,设了拒启;7.57.0 起 `GET /v1/config/catalog` 的回显也归一成公名 `mysql`,不再吐内部标签 `tidb`) |
|
|
144
|
+
| `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(fail-closed)。`CONFIG_PROVIDER=local` 下 `config.d/remote-exec.json` **压过**本 env(刻意:单机形的真源是那份文件);7.57.0 起这次抢占**响亮**——`remote_exec_lane_preempted_by_file` 点名被抢占的 lane、最终生效的 lane,以及它是不是一次**隔离降级**(隔离 lane 被换成不隔离的);文件点了尚未接线的隔离 lane(`e2b`/`k8s`/`ssh`/`adb`)时 env 腿照旧生效并说明 |
|
|
145
145
|
| `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | 未设 | **SSH 通道主机密钥校验**(仅 `REMOTE_EXEC=ssh`)。前者 = 主机公钥的 SHA256 base64 指纹(`SHA256:` 前缀与 `=` 填充都可选);后者 = `known_hosts` 文件路径,按 `host` / `[host]:port` **逐字**匹配行。**任一在场 ⇒ 严格校验,不符即拒连**(拒因带两半指纹 + `ssh-keyscan` 取指纹指路一行);两只同时在场 ⇒ 合取(都要过)。**两只全缺席 ⇒ 每连接一条响亮 warn(`ssh_host_key_unverified`,中间人风险)后照常连** —— 批量部署通道的既定姿态,已登记为 P-DEBT fail-open(`docs/FAIL-OPEN-CENSUS.md`)。指纹拼错 / `known_hosts` 路径读不到 ⇒ **启动即拒**。`known_hosts` 里**刻意不支持**:hashed(`\|1\|…`)行、通配符 pattern、`@cert-authority` —— 一律跳过,于是只有这些条目的主机会被**拒连**(`@revoked` 照常生效,且无论写在哪一行都优先于普通匹配行) |
|
|
146
146
|
| `HOOKS_TIMEOUT_MS` | 未设(引擎缺省 600000) | 每一只 hook 席位**单次调用**的时间上限(core `Hooks.timeoutMs`)。未设 = 用引擎自己的缺省(本服务不复制上游默认值)。`0` **按字面生效**(每个席位立即到期)。非整数 / 负数 / 超 `setTimeout` 上限(2147483647)⇒ **启动即拒** |
|
|
147
147
|
| `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面) |
|
package/USAGE.md
CHANGED
|
@@ -109,6 +109,14 @@ MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;词
|
|
|
109
109
|
# (主模型 5xx、以及兜底的一般 http 错误类也切便宜模型)。
|
|
110
110
|
# 不想要这两类触发就显式设回 rate_limit,breaker_open。拼错词 boot 拒启(不静默忽略)。
|
|
111
111
|
```
|
|
112
|
+
- 🔴 **思考档两键自 7.57.0 起是闭集拒启族**(此前拼错**静默丢**):`MODEL_DEFAULT_THINKING` 收
|
|
113
|
+
`minimal|low|medium|high|xhigh|max`(llm-core `ThinkingLevel` 六档)加哨兵词 `off`(= 显式「本部署
|
|
114
|
+
不设模型级默认档」,与不设同义);`MODEL_REASONING_EFFORT_LEVELS` 收这六档的**子集**(CSV,声明
|
|
115
|
+
「本 openai 端点吃得下哪些档」,高档 clamp-down 不 422;`off` 不是档位,写在这里拒启并点破)。
|
|
116
|
+
未知词 **boot 拒启并点名坏词**——与 `MODEL_DEGRADE_ON` 同律:`hight` 此前静默落成「无默认档」、
|
|
117
|
+
`med,high` 此前静默窄成只剩 `high` 一档,而这根旋钮的全部用途就是宣告高档可用,静默落空 = 高档
|
|
118
|
+
永远上不去。空串 / 纯分隔(`, `)仍与不设同义(不铸键,行为不变);anthropic 腿刻意不铸 compat
|
|
119
|
+
(那条腿不吃 `reasoning_effort`),`GET /v1/config/catalog` 对它诚实回 `null`。
|
|
112
120
|
- 反应式降级 brain 包在**最外层**;fallback brain 用自己的凭据(decorator 清掉主模型的 per-call key 防外泄给别的 provider)。**坑**:`MODEL_DEGRADE_TO` 最好别和被限流的是同一网关/账号,否则反应式切过去照样撞同一个 rate_limit。
|
|
113
121
|
- **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens**,默认 0 = `costUsd` 读 0)。云模型(如 review-gw 的 deepseek-v4-flash)必须设,否则 spend 恒为 $0;本地自托管(qwen)留 0 即对(无 per-token 外部花费)。字段名是 `costUsd`,**非美元计价的网关要先折算**(如 DeepSeek 官方 CNY ÷ 汇率)。配置控制面管的模型走 `CenterModel.cost`(中心存价、不存 secret)。
|
|
114
122
|
- 成本计量**自动开**:从 config 的 `model.cost`(per-1M 绝对 USD)注入 `pricing`,core 算出权威的整数 `costMicroUsd`(避免浮点累计误差)。`/metrics` 新增:`model_cost_micro_usd_total{model}`(覆盖所有 brain 调用=主任务+异步+council 子任务的总花费)、`brain_first_token_ms`(网关 hang 早警)、`brain_call_latency_ms`、`tool_calls_total{name,ok}`、`budget_exceeded_total{code}`、`cost_quota_rejected_total`、`degraded_total{reason}`(1.40 降级)。
|
|
@@ -215,7 +223,7 @@ MODEL_ID=<your-model-name> MODEL_CHEAP_ID=<your-cheap-model-name>
|
|
|
215
223
|
- 调用方可在 `objective` 里 `@<模型名>` 选模型,**只认已配置的名单**(`MODEL_ID` / `MODEL_CHEAP_ID`)——注入不了 `baseUrl`/`apiKey`;无 `@` → 默认模型。
|
|
216
224
|
- **`GET /v1/models`**(带 Bearer)→ `{models:[{name,id,provider,reasoning,vision}],default}`,只含名字/能力、**不含密钥** —— 消费方(如 OA)拿它做 `@` 自动补全下拉的数据源。
|
|
217
225
|
- 不设 `MODEL_CHEAP_ID` → 所有角色 = 主模型(行为不变)。
|
|
218
|
-
- **1M 双窗**(`autoCompactTokens`):`967000` 只发给 **claude-sonnet-5 系 id ∧ 窗 ≥1M**(CC 的 per-model 值,不是窗宽几何)。其他 ≥1M 模型要双窗触发点请显式配 —— 主槽 `MODEL_AUTO_COMPACT_TOKENS=<tokens>`、cheap 槽 `MODEL_CHEAP_AUTO_COMPACT_TOKENS=<tokens>`(**两槽各配各的**:主槽的值按主模型 id/窗声明,不会外溢到 cheap)。不配 → core 按整窗的 `W-33000` 平几何触发,boot 期发 `auto_compact_window_not_derived_1m` warn 点名槽位(`slot=main|cheap`)。⚠️ cheap 槽不设 `MODEL_CHEAP_CONTEXT_WINDOW` 时**继承主模型的窗**,主模型是 1M 则 cheap 也按 1M 计。取值范围:**必须低于该槽的物理窗**(CC 的形是 `窗-33000`)——
|
|
226
|
+
- **1M 双窗**(`autoCompactTokens`):`967000` 只发给 **claude-sonnet-5 系 id ∧ 窗 ≥1M**(CC 的 per-model 值,不是窗宽几何)。其他 ≥1M 模型要双窗触发点请显式配 —— 主槽 `MODEL_AUTO_COMPACT_TOKENS=<tokens>`、cheap 槽 `MODEL_CHEAP_AUTO_COMPACT_TOKENS=<tokens>`(**两槽各配各的**:主槽的值按主模型 id/窗声明,不会外溢到 cheap)。不配 → core 按整窗的 `W-33000` 平几何触发,boot 期发 `auto_compact_window_not_derived_1m` warn 点名槽位(`slot=main|cheap`)。⚠️ cheap 槽不设 `MODEL_CHEAP_CONTEXT_WINDOW` 时**继承主模型的窗**,主模型是 1M 则 cheap 也按 1M 计。取值范围:**必须低于该槽的物理窗**(CC 的形是 `窗-33000`)—— 高于物理窗时 **core 在取数处把触发窗夹回物理窗**(`resolveTriggerWindow`,自 core 5.60.0 起),即**这根旋钮变成 no-op**、几何回落成没配它的样子,并由引擎逐任务铸一条 `config.autocompact_window_clamped` 通告(受众 operator)。server 侧**不夹也不拒**(夹紧归 core 的取数处,一处一次),但会在 **boot** 就发 `auto_compact_tokens_above_context_window` warn 点名槽位、越窗值与 **`clampedTo`(core 会夹到的那个数)**—— 不必先派一条任务才知道旋钮白配了。⚠️ 7.57.0 之前这一段写的是「autocompact 事实上关掉、改由破坏性 guard 砍历史」,那是 core 5.60.0 之前的行为,已订正。
|
|
219
227
|
|
|
220
228
|
**可选 — 开发者模式 building blocks(core 1.43,默认中立)**
|
|
221
229
|
```bash
|
|
@@ -273,7 +281,7 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
|
|
|
273
281
|
| `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS` | `10000`(`[500, 600000]`) | 目录取不到之后的退避窗(只用于取数失败臂,不用于负结果) |
|
|
274
282
|
| `MEMORY_DELEGATION_EVIDENCE` | 缺省(=引擎自缺省 `static-face`) | **委派臂的证据标准**(core 5.45.0 起)。`static-face`=今日行为:委派的 attestation 缺失/未知**且**静态工具面够得着外部内容 ⇒ 标记本会话记忆为已污染(可能性即暴露)。`attested-only`=**只**豁免那一条静态面标记,且**真的豁免了一次**才响亮通告(`engine_notice` 的 `memory.delegation_static_mark_waived`,每个 prepared leg **至多**一次);送达 `external` attestation 照标、非委派的污染类工具照标、委派工具自身被分类为污染的照标。⚠️ 通告**不是 leg 计数器**:不含委派的 leg、污染面没挂载的 leg、送达了 attestation 的委派都不发 —— 看不到通告的常态含义是「没有可豁免的事」,不是「配置坏了」。坏值**启动期拒**。**部署席 only**(TaskSpec 无同名键,任务/受治脚本无法据此放松部署)。⚠️ **已接受的代价**:`attested-only` 下**后台**子代的真实外部接触不标记本会话(其内容经 TaskOutput / 任务通知注入 / AgentTranscript 摘要回流,三条都不带 attestation);异常收尾的前台子代同理。已标记的会话永不回滚清洗。运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.delegationEvidence`(`null`=本部署未设,引擎自缺省) |
|
|
275
283
|
| `MEMORY_PROVENANCE` | 缺省(=引擎自缺省 `carry`) | **记忆 provenance 总开关**(core 5.46.0 起)。一个会话被判为**已暴露**之后,它的记忆写怎么处置。`carry`=引擎缺省:**普通**记忆写照常提交,但带上引擎铸的 `origin` 标记(随条目走后端/同步/导出包);指令形文件(feedback / pinned / triggers / applies-when)仍被扣下隔离;派生索引的会话散文照回滚;内容扫描门原样跑(**标记不是豁免**)。`off`=此前的行为:不铸 origin 标记,已暴露会话的 harvest **一条都不收**(整体隔离候人审)。⚠️ 已提交的标记在编辑时照样带下去 —— `off` 停的是**铸**,从不抹掉已记的事实。与 `MEMORY_DELEGATION_EVIDENCE` **正交**(四种组合全合法):那一键决定**什么时候**判会话为已暴露,本键决定判了之后**对写做什么**。坏值**启动期拒**(`OFF`/`false`/`none` 这类「看起来像关掉」的写法一律不折成缺省 —— 静默回默认 = 一台自认为「暴露会话零收录」的部署其实在照常收录)。**部署席 only**(TaskSpec 无同名键,任务/受治脚本无法据此改姿态);**不冻进 checkpoint**(resume 腿跟当前部署配置走)。运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.provenance`(`null`=本部署未设,引擎自缺省) |
|
|
276
|
-
| `MEMORY_CAPTURE_POLICY` | 缺省(=引擎自缺省 `open`) | **记忆采集 opt-out 的部署姿态**(core 7.0.0 起)。任务可带 `memory.capture:"off"` 声明「本会话不进长期记忆」(一次性,resume 恒不采集,无反向拼写),本键决定怎么裁那条声明:`open`=按面值生效,**只有**该 principal 的显式 `false` verdict 拒(403 `memory.capture_optout_denied`),resolver 故障**放行**+披露(隐私轴 fail-safe:错拒=记了用户明说不要记的会话,不可逆);`governed`=verdict **必答**(混合车队:部分用户强制留存),故障/缺席**拒跑**(fail-closed);`capture-required`=全体声明拒(一行配置)。verdict
|
|
284
|
+
| `MEMORY_CAPTURE_POLICY` | 缺省(=引擎自缺省 `open`) | **记忆采集 opt-out 的部署姿态**(core 7.0.0 起)。任务可带 `memory.capture:"off"` 声明「本会话不进长期记忆」(一次性,resume 恒不采集,无反向拼写),本键决定怎么裁那条声明:`open`=按面值生效,**只有**该 principal 的显式 `false` verdict 拒(403 `memory.capture_optout_denied`),resolver 故障**放行**+披露(隐私轴 fail-safe:错拒=记了用户明说不要记的会话,不可逆);`governed`=verdict **必答**(混合车队:部分用户强制留存),故障/缺席**拒跑**(fail-closed);`capture-required`=全体声明拒(一行配置)。verdict 源**两条**(析取):①SQL 店后端上的授权表 `memory_optout_grant`(三层折叠:per-principal 行 → 部署缺省行 → 代码缺省 allow;管理面 `GET|PUT /v1/admin/memory-optout`(PUT=部署缺省行)· `PUT|DELETE …/:principal`,operator-only,能力位 `memoryOptOutGrant`,零缓存写后即生效);②非 dry-run 的 config center 在该 principal 的 entitlement caps 里发 `allowMemoryOptOut`。🔴 **`governed` 而两源皆无 ⇒ 启动期拒启**(无源 = 每个声明都被拒且没人知道为什么;接 `DB_BACKEND=mysql|pg`、接非 dry-run center,或改姿态)。⚠️ **center-only 源的语义**(只配了 center、没有 SQL 授权表):`allowMemoryOptOut` 在 entitlement schema 里是**可选键** —— center 不发它 = 该 principal 的 verdict **缺席** = `governed` 下每次 opt-out 恒拒(403 `memory.capture_optout_denied`),而 worker **照常起动**;这一形部署级证不死(per-principal 授权只有运行期才知道),所以 server 两处响亮:启动期一行 `memory_capture_governed_center_only_source`、运行期首次真缺席一行 `memory_capture_governed_verdict_absent`(每进程一次,纯披露不改判)。要确定性 verdict 就接 SQL 授权表(它三层折叠后恒答显式 boolean)。坏值**启动期拒**(`Governed`/`required`/`off` 一律不折成缺省:两种姿态的故障极性**相反**)。**部署席 only**;**不冻进 checkpoint**。运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.capturePolicy`(`null`=本部署未设)。记录载体(core 7.0.2 起):SQL 后端(`DB_BACKEND=mysql\|pg`)上记录落 `session_capture_optout` 表(跨副本存活,随中央 schema 自建零旋钮)——**远端车道(e2b/k8s)的 `capture:"off"` 声明自此真持久**;无 SQL 店的远端形仍 409 `config.memory_capture_unsupported`(诚实拒:落不进 resume 副本可见的载体就不许诺);单机本地车道走 core 文件三件套照常。用户面四口:提交键 `memoryCapture:"off"`(声明形)· `POST /v1/runs/:id/memory/capture-optout`(live 中途翻转,owner 门)· `POST /v1/sessions/:id/memory/erase`(所有者自助抹除,select 硬限本会话)· `GET /v1/sessions/:id/memory-status`(状态读面,owner 门,五可选键诚实缺席)。契约全文 `docs/ASSISTANT-WIRE-CONTRACT.md` §12 |
|
|
277
285
|
|
|
278
286
|
⚠️ **拒启动**:多租户 + 记忆面点亮 + `projects[].defaultScopes` 里有 org 键 + 目录源缺席 ⇒ 启动报错
|
|
279
287
|
并点名 projectId(该部署的每个此类请求都会在 prepare 期整拒,响亮拒启动比静默全拒服务诚实)。
|
|
@@ -446,11 +454,16 @@ QUESTION_TTL_MS=300000
|
|
|
446
454
|
|
|
447
455
|
```bash
|
|
448
456
|
# 协议总开关。默认 true;显式 =false 是**本协议面**的干净还原键(不发 approval_request、不落 ask 行、
|
|
449
|
-
#
|
|
450
|
-
# 回滚键是下面的 UNATTENDED_APPROVAL_POLICY=deny(两根旋钮正交,别当一根用)
|
|
457
|
+
# 不起收敛器腿)。⚠️ 它**不**回滚 7.34.0 的 R-13 窗到期语义——那一条的
|
|
458
|
+
# 回滚键是下面的 UNATTENDED_APPROVAL_POLICY=deny(两根旋钮正交,别当一根用);它也**不**再回滚窗长
|
|
459
|
+
# (窗是活卡件不是协议件,见下一键——缺省值与旧硬编码窗同为 300000,没配过就仍然字节不变)
|
|
451
460
|
STREAM_APPROVAL_ENABLED=true
|
|
452
|
-
# 活卡窗(毫秒,默认 300000=5min)
|
|
453
|
-
#
|
|
461
|
+
# 活卡窗(毫秒,默认 300000=5min)。**辖域=所有腿**:协议上不上场都按它计窗——local 后端 /
|
|
462
|
+
# 无 checkpoint 店 / 关了协议的部署同样生效(此前那三形上本键一个字节都不生效、窗恒 5min,且没有任何
|
|
463
|
+
# 一行说出来)。0 = 运维显式关窗 ⇒ **一张卡都不发**、恒走无人值守终局(不是"还原",还原用上面那个键;
|
|
464
|
+
# 缺省 park 政策 + park 设施在场 ⇒ 全落 durable park 经 /v1/approvals/:sessionId/decide 兑现;
|
|
465
|
+
# 配了 UNATTENDED_APPROVAL_POLICY=deny 或没有 park 设施的部署这里是"恒当场拒"= 受门工具全关,
|
|
466
|
+
# boot 期打一行 stream_ask_window_zero 点名)。负值**拒启**(此前静默等价于 0 还白发一张没人赢得了的卡)
|
|
454
467
|
STREAM_ASK_WINDOW_MS=300000
|
|
455
468
|
# 协调器窗长三元的安全余量(毫秒,默认 10000):有效窗 = min(ttl, 本 leg 余量 − 本值)
|
|
456
469
|
STREAM_ASK_WINDOW_MARGIN_MS=10000
|
|
@@ -481,7 +494,11 @@ STREAM_APPROVAL_ORPHAN_TTL_MS=604800000
|
|
|
481
494
|
UNREACHED_ASK_TTL_MS=3600000
|
|
482
495
|
```
|
|
483
496
|
- **坏形一律启动期炸,不静默折默认**:五个 `numEnv` 键(`STREAM_ASK_WINDOW_MS` / `STREAM_ASK_WINDOW_MARGIN_MS` /
|
|
484
|
-
`STREAM_APPROVAL_REPLAY_MAX` / 两个 `ADMIT_MAX_*`)
|
|
497
|
+
`STREAM_APPROVAL_REPLAY_MAX` / 两个 `ADMIT_MAX_*`)非数字即拒启;`STREAM_ASK_WINDOW_MS` 另加一道
|
|
498
|
+
**非负门 + 上界门**(负值此前静默落成「立即到期」并白发一张没人赢得了的卡;超过 Node 定时器
|
|
499
|
+
上界 `2147483647`(2^31−1 ms ≈ 24.8 天)的值被 `setTimeout` 静默折成 1ms —— 想调长反而立即到期,而跨旋钮
|
|
500
|
+
门只管 `窗 + 宽限 < orphanTtl`、orphanTtl 域上界是 90 天,所以 30 天的窗此前合法通过。两侧一律点名拒启,
|
|
501
|
+
不夹取;`0` 仍是合法的「显式关窗」姿态);四个收敛器键 + `UNREACHED_ASK_TTL_MS`
|
|
485
502
|
额外**有界**,越界拒启(不夹取);`STREAM_APPROVAL_RECONCILE_BATCH` 再加一道整数门(`200.5` 会一路走到
|
|
486
503
|
SQL `LIMIT` 上)。
|
|
487
504
|
- **🔴 跨旋钮不变量**:`STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS` 必须**严格小于**
|
|
@@ -507,6 +524,11 @@ UNREACHED_ASK_TTL_MS=3600000
|
|
|
507
524
|
窗满**自动 deny(fail-closed)后 run 继续**,不会死锁在等一张没人能回的卡上;代价是断连期间用户的
|
|
508
525
|
批准机会直接过期。(7.34.0 起这条 deny 由**引擎**在「没有可停靠的 durable 门」时给出——服务侧只报
|
|
509
526
|
「此刻无人可答」,见上面的 `UNATTENDED_APPROVAL_POLICY`;结局不变,拒绝理由的文案更准确。)要「断连也不丢决策」的语义,配齐 durable 前置(持久 store + `DURABLE_APPROVAL=true`)。
|
|
527
|
+
⚠️ **这条腿上的窗照样是 `STREAM_ASK_WINDOW_MS`**:此前它被硬编码成 5min、旋钮在这些部署上
|
|
528
|
+
静默无效(慢组场景只能真等满 5 分钟);现在调它就是调这条腿。另:这类部署上**迟到决议**
|
|
529
|
+
(卡挂过窗、人才按 Yes)不能走 `POST /v1/tool-approvals/:id/respond`(没有持久 ask 行 ⇒ 恒 404
|
|
530
|
+
`tool_approval.not_pending`、不带 `cause`),要走 `POST /v1/approvals/:sessionId/decide`
|
|
531
|
+
——前提是配了 `DURABLE_APPROVAL=true`(local 车道有文件形 checkpoint 店),否则窗到期即 fail-closed 拒。
|
|
510
532
|
默认值(300s + 60s vs 7d)自然满足,只有显式改坏才会撞上。
|
|
511
533
|
- **调参方向**:想让人有更长时间点审批卡 → 调大 `STREAM_ASK_WINDOW_MS`;卡太多刷屏 → 调小两个 `ADMIT_MAX_*`
|
|
512
534
|
(代价是超限的 ask 走 park,要有人去审批队列捞);库压大 → 调小 `STREAM_APPROVAL_RECONCILE_BATCH`
|
|
@@ -884,7 +906,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
884
906
|
| `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) |
|
|
885
907
|
| `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
|
|
886
908
|
| `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。编排壳在发 SIGTERM **之前**调它;server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
|
|
887
|
-
| `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
|
|
909
|
+
| `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 原文。 |
|
|
888
910
|
| `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。 |
|
|
889
911
|
| `GET /health` | 健康(无需鉴权)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
|
|
890
912
|
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
@@ -1166,7 +1188,12 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
1166
1188
|
**进程警告面(日志采集方注意)**:引擎(core ≥5.16)在持久 agent 行的 org 判决写回被丢弃等场合走
|
|
1167
1189
|
Node `process.emitWarning`,警告文案前缀固定 `sema durable-agents:`——采集规则按该前缀匹配,别当噪音过滤掉。
|
|
1168
1190
|
server 自身的兜底留痕走 stderr 契约行 `[sema] fail-open: <tag>`(逐 tag 一次)+ `fail_open_total` 指标,
|
|
1169
|
-
tag 词表与每条的最坏后果见 `docs/FAIL-OPEN-CENSUS.md
|
|
1191
|
+
tag 词表与每条的最坏后果见 `docs/FAIL-OPEN-CENSUS.md`。引擎装配期忠告(core `onError(phase:"config")`)落
|
|
1192
|
+
`runner_config_advisory` 日志行,带结构化 `code`(如 `config.toolpolicy.unmatched_names`)与 `classification`
|
|
1193
|
+
(core 给宿主定级的判别词;词表外原样透传并标 `classificationKnown:false`),指标 `runner_config_advisory_total`
|
|
1194
|
+
按 `code` / `classification` / `level` 打标;其中 `shell-gate-off` 在本 session 显式声明 `permissionMode:
|
|
1195
|
+
"bypassPermissions"` 时是调用方要的姿态,降为 **info** 并标 `expected`——只有没表态落到引擎默认 off 的无壳直连
|
|
1196
|
+
形才 warn。
|
|
1170
1197
|
|
|
1171
1198
|
### 9.1 limits.approachNotice(6.1.0 起)
|
|
1172
1199
|
|
|
@@ -1186,6 +1213,19 @@ CC 逐字节形的提醒帧:`<total_tokens>N tokens left</total_tokens>`(**缺
|
|
|
1186
1213
|
- **分辨依据**(消费端/日志过滤):该帧内文**恒以 ` tokens left` 结尾**;`Infinite` 常量臂(`infinite`
|
|
1187
1214
|
档)是唯一例外,单独放行——它不带数字、不依赖上限,且只在显式 `true` 下可达。
|
|
1188
1215
|
|
|
1216
|
+
### 9.1c `TASK_TIMEOUT_SEC` 的真语义:`0` 不是「关」,正值**只抬不降**(7.57.0 起成文)
|
|
1217
|
+
|
|
1218
|
+
这根旋钮此前在配置目录里写着「0=关」——那句**只在单用户部署成立**,而且漏了它的第二条纪律。
|
|
1219
|
+
真判据在 `taskWallClockSec`:
|
|
1220
|
+
|
|
1221
|
+
- **`0` / 未设** ⇒ 走 **tenancy 基值**:单用户 turnkey(`REQUIRE_PRINCIPAL` 未开)= **真无墙**;
|
|
1222
|
+
多租户 = **2400s**(大任务 council/debate/discuss = **3600s**)——墙**是开着的**,不是关的。
|
|
1223
|
+
- **设了正值** ⇒ `max(值, 基值)` = **只抬不降**。所以在多租户上写 `TASK_TIMEOUT_SEC=60` 拿到的是
|
|
1224
|
+
**2400**,不是 60(设计如此:误配的低值不许砍掉 council 预算)。要**更紧**的配速走提交体
|
|
1225
|
+
`limits.maxWalltimeMs`——那是 caller 的显式意图,直接生效,可以低于 env 墙。
|
|
1226
|
+
- **负值** ⇒ **启动即拒**(7.57.0 起)。此前 `-1` 被静默折成 `0` = 「按 tenancy 走」,运维想关墙、
|
|
1227
|
+
在多租户上反而拿到 2400s 的墙——方向相反且零提示。非数值(`3600s` 这种带单位后缀的写法)照旧拒启。
|
|
1228
|
+
|
|
1189
1229
|
### 9.2 部署级治理窗(USAGE_WINDOWS,6.1.0 起;$ ceiling=core 7.0.0 起)
|
|
1190
1230
|
|
|
1191
1231
|
跨任务的滑动预算(core `usageWindows`),缺省不设=关。单键 JSON 数组;每窗两只**独立** ceiling
|
|
@@ -1227,3 +1267,55 @@ USAGE_WINDOWS='[{"windowMs":18000000,"maxTokens":5000000,"maxCostUsd":10,"anchor
|
|
|
1227
1267
|
k8s 沙箱(pod `activeDeadlineSeconds` 真硬死线)向引擎声明 `lifetimeMs`——死线前 ~60s 引擎在 turn
|
|
1228
1268
|
边界尝试 suspend(reason `env_lifetime`,基建合格时)而不是被平台杀在半途;单个 >60s 的工具调用仍可能
|
|
1229
1269
|
冲过该窗口,声明是尽力座不是保证。e2b 沙箱有 keepalive 滚动续期、无固定死线,不声明(行为不变)。
|
|
1270
|
+
|
|
1271
|
+
### 9.4 auto 模式(`permissionMode:"auto"`):武装三态、本地 org deny、自查(与 core 的意图武装式合修)
|
|
1272
|
+
|
|
1273
|
+
**背景**:此前引擎只在「组织授予」(config-center 解出的 per-principal `runtimeCaps.autoMode === true`)时武装
|
|
1274
|
+
auto 分类器,而无 center 的本地/自托管部署没有任何授予路径 ⇒ 壳选了 auto,引擎跑的仍是 default + 静态只读
|
|
1275
|
+
白名单(真实用户诊断属实)。core 的新武装式对齐 CC 的「用户开 + 组织拒」极性:
|
|
1276
|
+
|
|
1277
|
+
```
|
|
1278
|
+
armed ⟺ 本次请求 permissionMode 是 "auto" ∧ 本部署装配了分类器席位 ∧ runtimeCaps.autoMode !== false
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
**`runtimeCaps.autoMode` 三态**:`undefined`(没配 center / center 未配这个 principal)= **不阻**;`true` = 不阻;
|
|
1282
|
+
`false` = **组织拒**。来源三条,都落同一个位:① center 对该 principal 显式 `false`;② center 配了但**硬失败**
|
|
1283
|
+
(fail-closed 拒项同车铸 `false`,与 workflows/fork 同向,`denyTtl` 窗内不抬起);③ 本地旋钮:
|
|
1284
|
+
|
|
1285
|
+
| 旋钮 | 缺省 | 语义 |
|
|
1286
|
+
|---|---|---|
|
|
1287
|
+
| `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 轴) |
|
|
1288
|
+
|
|
1289
|
+
**自查读面**(`GET /v1/capabilities.permissionModeAuto`,壳的 `sema doctor permissions` 消费;下例第一行是
|
|
1290
|
+
`intentArming:true`(与带新武装式的 core 同批提货后)的读数——server 7.57 × core 7.2.0 上无 center 的本地部署答
|
|
1291
|
+
`intentArming:false, armed:false, reason:"deployment_incapable"`,见 `reason`/`intentArming` 条):
|
|
1292
|
+
|
|
1293
|
+
```
|
|
1294
|
+
GET /v1/capabilities?permissionMode=auto
|
|
1295
|
+
→ permissionModeAuto: { accepted:true, classifierSeat:true, entitlementSource:false, intentArming:true, armed:true, model:"default" }
|
|
1296
|
+
GET /v1/capabilities?permissionMode=default
|
|
1297
|
+
→ permissionModeAuto: { …, armed:false, reason:"mode_not_auto" }
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
- `armed`:按上式此刻会不会武装(`?permissionMode=` 折进本次意图;查询串缺席 = 按 auto 意图答「若请求会不会武装」;
|
|
1301
|
+
五词之外 400 `request.field_invalid`)。
|
|
1302
|
+
- `reason`(未武装时,闭集**六词**,判序即武装式合取序):`mode_not_auto`(意图不是 auto)/ `deployment_incapable`
|
|
1303
|
+
(装配面没挂分类器席位——生产装配恒挂,桩与一次性 CLI 不挂;**或** `intentArming:false` 且本部署没配 center:
|
|
1304
|
+
旧式「组织授予」极性下零授予路径,真武装不了)/ `org_denied`(上面三条来源之任一,以及 `intentArming:false` 下
|
|
1305
|
+
center 在场但本 principal 未被授予——旧极性自己的「缺席=未授予」)/ `local_denied`(本地旋钮 `PERMISSIONS_DISABLE_AUTO_MODE=true`,
|
|
1306
|
+
改本机 env)/ `settings_denied`(settings 层 `permissions.disableAutoMode:"disable"`——本面不产,归 run 级面)/ `resolver_fault`
|
|
1307
|
+
(per-principal resolver 抛错,引擎同形全拒)。
|
|
1308
|
+
- `intentArming`:这对 server+core 走哪套极性,由安装包探针答——`true` = 意图武装式(无 center 也能 auto);`false` = 旧式「组织授予」
|
|
1309
|
+
(npm core 7.2.0 下 `false`:此时 `armed` 仍是**当前真值**——center 授予了的 principal 答 `true`,因为它
|
|
1310
|
+
真在武装——且**不看查询串**,旧式 core 不读 permission mode,授予即武装,`mode_not_auto` 只在新式下出现;无 center
|
|
1311
|
+
的本地部署答 `false`/`deployment_incapable`)。
|
|
1312
|
+
- `model`:分类器**配置**路由(role 回落序 `classifier → summarize → default` 的 catalog 名),不是本次真用的模型。
|
|
1313
|
+
- `entitlementSource` 仍只说「center 源在不在」;新武装式下它为 `false` **不再**意味着 auto 不武装。
|
|
1314
|
+
- 读面按调用方身份解 per-principal caps(与引擎同一只 resolver,per-principal 缓存 60s;center 硬失败时受 8s
|
|
1315
|
+
超时 + 10s deny 缓存界)。
|
|
1316
|
+
|
|
1317
|
+
**settings 层 kill-switch(件⑥)**:请求体 `settings.permissions.disableAutoMode` 受理 **`"disable"` / `true` / `false`** 三形(两套已发布契约的拼写:CC 250 的严极词 `"disable"`,与 `@sema-agent/sdk` `SettingsPermissions.disableAutoMode?: boolean`);受理集之外的值 400 `request.field_invalid`。
|
|
1318
|
+
`"disable"` 与 `true` **同义**:把该 run 的生效模式由 auto 折成 default(座不写 ⇒ 不武装;tighten-only,只能关不能开);`false` = 显式「不禁」= **合法且无效果**(本键没有放宽臂——它不是 auto 的开关)。`disableBypassPermissionsMode` 本批不落点。
|
|
1319
|
+
|
|
1320
|
+
**per-run 面**:本批不在 run 记录上加 per-run `autoMode` 座——引擎侧的武装结果 / 未武装原因(含只有引擎知道的熔断类)
|
|
1321
|
+
将随 core 的状态座到货,届时 server 只投影不自算;此前 `armed` 是 server 侧三项可知判据的裁决。
|
|
@@ -28,13 +28,49 @@ export interface AuditSettleInput {
|
|
|
28
28
|
settledBy?: string;
|
|
29
29
|
settledAtMs: number;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* [ref] **迟到人工结算点**入参:调用方(两条 HTTP decide 腿)只交 **checkpoint 锚 + 这次结算的事实**,
|
|
33
|
+
* `approvalId` 的反查与「算不算迟到」的判据**属主在店内**(一处成文,见 {@link FileApprovalAskAuditStore.recordLateSettle})
|
|
34
|
+
* —— 路由层没有账本索引,让它自己推会造出第二份判据。
|
|
35
|
+
*/
|
|
36
|
+
export interface AuditLateSettleInput {
|
|
37
|
+
/** checkpoint 锚①:被决议那条 durable 行的 sessionId(`/v1/approvals/:sessionId/decide` 的那把;
|
|
38
|
+
* parked 子代形下 = **子会话 id**,与 ask 行的 `sourceTaskId` 同值)。 */
|
|
39
|
+
sessionId: string;
|
|
40
|
+
/** checkpoint 锚②:`pendingAction.toolCallId`(tool_approval 门恒在场;别的审批门型可缺席)。 */
|
|
41
|
+
toolCallId?: string;
|
|
42
|
+
/** checkpoint 锚③:resume 凭据的**哈希**({@link buildCheckpointAnchorHash};token 本身是 token-as-auth
|
|
43
|
+
* 的能力凭据,**绝不**落审计盘 —— 账本 0o600 也不是把凭据写进去的理由)。 */
|
|
44
|
+
checkpointTokenHash?: string;
|
|
45
|
+
/** 这次人工结算的结果(approve/deny 逐字映射;`expired` 不是本记录的合法值 —— 它是流窗的词)。 */
|
|
46
|
+
outcome: "allowed" | "denied";
|
|
47
|
+
/** core `ApprovalSettledBy` 逐字(调用方给,店**不派生**:集中推导正是「窗到期被报成另一个人拒绝」
|
|
48
|
+
* 的成因,[ref] 件6③ 同律)。今天两条腿恒 `"human"`,但真值以调用方为准。 */
|
|
49
|
+
settledBy: string;
|
|
50
|
+
/** 记录时刻 = 那条腿**权威受理**的时刻(不是请求到达的时刻)—— 落行点在 200 之后,见记录种类表。 */
|
|
51
|
+
settledAtMs: number;
|
|
52
|
+
}
|
|
31
53
|
/** 协调器消费的窄写口(协调器只 import type,不拖实现依赖)。 */
|
|
32
54
|
export interface ApprovalAskAuditSink {
|
|
33
55
|
recordAsk(input: AuditAskInput): void;
|
|
34
56
|
recordSettle(input: AuditSettleInput): void;
|
|
35
57
|
/** 腿闭点(`runWithContext` finally)。只为铸过审计行的 taskId 真落行(店内去重)。 */
|
|
36
58
|
noteLegClosed(taskId: string): void;
|
|
59
|
+
/** [ref] 迟到人工结算点(HTTP decide 两腿)。**纯观察**:自吞一切失败,不返回受否,绝不改变结算面。 */
|
|
60
|
+
recordLateSettle(input: AuditLateSettleInput): void;
|
|
37
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* [ref]:decide 两腿消费的**更窄写口** —— 只有迟到结算这一个动词。
|
|
64
|
+
* 刻意不把整个 {@link ApprovalAskAuditSink} 递给 HTTP 层:铸造 / 决议 / 腿闭三点的属主是协调器,
|
|
65
|
+
* 路由层拿到它们只会长出第二个写者(与 {@link ApprovalAskAuditReadFace} 同一条窄口纪律)。
|
|
66
|
+
*/
|
|
67
|
+
export type ApprovalLateSettleSink = Pick<ApprovalAskAuditSink, "recordLateSettle">;
|
|
68
|
+
/**
|
|
69
|
+
* checkpoint resume 凭据 → 审计行可落的锚(sha256 前 16 hex)。
|
|
70
|
+
* 单向、不可逆、不足以拿去 resume;够用来把审计行 join 回 checkpoint 平面(取证时对手上的 token 现算)。
|
|
71
|
+
* 形与长度的唯一属主在此(两条腿都调它,不各自 `createHash`)。
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildCheckpointAnchorHash(token: string): string;
|
|
38
74
|
/** v1.1 §6 两分臂(闭集)。 */
|
|
39
75
|
export type CrashOrphanState = "pending" | "decided";
|
|
40
76
|
/** `GET /v1/approvals` additive 键 `crashConverged` 的行形(wire 键集刻意窄:args/卡体不上列表,
|
|
@@ -63,6 +99,15 @@ export interface CrashConvergedRow {
|
|
|
63
99
|
export interface ApprovalAskAuditReadFace {
|
|
64
100
|
listCrashConverged(scope: string | undefined): CrashConvergedRow[];
|
|
65
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* [ref]:`late_settle` 行上 `approvalId === null` 的**成因词**(闭集;缺席 = 反查唯一命中,身份是真的)。
|
|
104
|
+
* 「反查不到不得静默」:身份说不清的时候写的是「说不清 + 为什么」,不是不写、也不是编一个 id。
|
|
105
|
+
* 闭集纪律([ref] + 本店 `crash_converged.orphanState` 先例):重放读到**词表外**的值 ⇒ 整行折坏行计数,
|
|
106
|
+
* 不折 `undefined` —— 折了就等于把「说不清且原因是 X」静默降级成「说不清、没原因」。
|
|
107
|
+
* 与身份字段的**交叉不变式**(写口自带,重放侧同样强制):`approvalId === null` ⇔ 本词在场;身份说不清
|
|
108
|
+
* 时 `taskId` 必须缺席(挑一条铸造行填 = 张冠李戴)。
|
|
109
|
+
*/
|
|
110
|
+
export type LateSettleUnresolved = "ambiguous_anchor";
|
|
66
111
|
export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink, ApprovalAskAuditReadFace {
|
|
67
112
|
/** 账本绝对路径(测试/取证按文件字节对账 —— 崩溃语义的真源是文件,不是内存投影)。 */
|
|
68
113
|
readonly ledgerPath: string;
|
|
@@ -83,6 +128,21 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
|
|
|
83
128
|
/** 本进程铸过审计行、且腿闭信号可达的 taskId(腿闭去重 + 防无关腿刷账本)。 */
|
|
84
129
|
private readonly openLegTasks;
|
|
85
130
|
private convergedOnce;
|
|
131
|
+
/**
|
|
132
|
+
* [ref] 反查索引 ①:**checkpoint 锚 → 铸造行身份**。与 {@link replayed} 刻意分家 —— 那张表是
|
|
133
|
+
* 「前世账本」快照(收敛只对前世行),而迟到决议要认的是**前世 + 本进程**两段账(人可以在重启前批,
|
|
134
|
+
* 也可以在重启后批)。键形 = {@link FileApprovalAskAuditStore.callAnchorKey}(会话轴的两把键见
|
|
135
|
+
* {@link FileApprovalAskAuditStore.indexAsk});弱桶另见下一个字段。
|
|
136
|
+
*/
|
|
137
|
+
private readonly askByCallAnchor;
|
|
138
|
+
/** [ref] 反查索引 ①b:**无 toolCallId** 的铸造行(只能按会话认);与 A 桶分开是为了不让「有 callId 的
|
|
139
|
+
* 锚」误命中「另一只没写 callId 的 ask」—— 归并会把两条不同的 ask 折成一条。 */
|
|
140
|
+
private readonly askBySessionOnly;
|
|
141
|
+
/** [ref] 反查索引 ②:走过 park 路由的窗到期(`settle{outcome:"expired", parkRouted:true}`)——
|
|
142
|
+
* 「这次决议算不算迟到」的**唯一**判据来源。 */
|
|
143
|
+
private readonly parkExpired;
|
|
144
|
+
/** [ref] 反查索引 ③:approvalId → 铸造坐标(命中后回填 late 行的 `taskId`)。 */
|
|
145
|
+
private readonly askTaskId;
|
|
86
146
|
/**
|
|
87
147
|
* boot 期 fail-stop:mkdir/重放失败**拒启**(File 店族同律)。root = local 数据根
|
|
88
148
|
* (`config.localDataRoot`;与 `LocalBackend` 同源,见头注装配段)。
|
|
@@ -103,11 +163,50 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
|
|
|
103
163
|
private group;
|
|
104
164
|
/** append + fsync(逐行落盘:进程猝死不丢已记的行 —— 本店的全部价值就在崩溃时刻)。
|
|
105
165
|
* 运行期失败**响亮不阻断**:审批可用性不押在审计盘上(盘满/只读时人还得能批),但丢行必须在遥测
|
|
106
|
-
* 显形([ref] 三件套,census 登记行同批)。
|
|
166
|
+
* 显形([ref] 三件套,census 登记行同批)。
|
|
167
|
+
* 🔴 返回**这条记录是否真的落盘并 fsync 成功**([ref] codex R1-F2 采):迟到结算的 late 判据只认
|
|
168
|
+
* 落了盘的前置事实 —— 调用方据此决定要不要把这条记录喂进反查索引(见 {@link recordAsk}/
|
|
169
|
+
* {@link recordSettle})。没有这个返回值,索引会把一条**只在内存里**的 ask/park 事实当证据,
|
|
170
|
+
* 后来铸出的 `late_settle` 行在同一个账本文件里无法复核。 */
|
|
107
171
|
private append;
|
|
172
|
+
/** [ref] 锚键 A(精确):会话 + 工具调用 id。会话轴登记**两把** —— 见 {@link indexAsk}。 */
|
|
173
|
+
private static callAnchorKey;
|
|
174
|
+
/**
|
|
175
|
+
* [ref]:把一条铸造行放进反查索引。
|
|
176
|
+
* 会话轴登记 `sessionId` 与 `sourceTaskId` **两把**:parked 后台子代的 checkpoint 上
|
|
177
|
+
* `cp.sessionId` 是**子会话 id**(= ask 行的 `sourceTaskId`),而 ask 行的 `sessionId` 是**宿主**
|
|
178
|
+
* 会话(铸造点取的是 `origin.sessionId`,继承链闭包携宿主身份)—— 只登记一把会让委派子代那条腿
|
|
179
|
+
* 的迟到决议恒反查不到。顶层 run 上两者同值,登记去重后是同一把。
|
|
180
|
+
*/
|
|
181
|
+
private indexAsk;
|
|
182
|
+
/** [ref]:决议行进 late 判据索引(只认 `expired ∧ parkRouted` —— 别的终局都不是「转 durable 等人」)。 */
|
|
183
|
+
private indexSettle;
|
|
108
184
|
recordAsk(input: AuditAskInput): void;
|
|
109
185
|
recordSettle(input: AuditSettleInput): void;
|
|
110
186
|
noteLegClosed(taskId: string): void;
|
|
187
|
+
/**
|
|
188
|
+
* [ref] **迟到人工结算点**(判据的唯一属主;两条 HTTP decide 腿只交锚,不自己推)。
|
|
189
|
+
*
|
|
190
|
+
* 三步:
|
|
191
|
+
* 1. **反查**:checkpoint 锚 → 铸造行身份。**锚带 `toolCallId` 时,`(会话, toolCallId)` 精确桶是
|
|
192
|
+
* 唯一权威**;两侧都没有调用锚时才退到会话弱桶(= 该会话下没写 toolCallId 的铸造行)。会话轴
|
|
193
|
+
* 登记两把(宿主会话 / 子会话,见 {@link indexAsk})。
|
|
194
|
+
* 🔴 **弱桶绝不与精确桶合并**(codex R1-F3 采,两条真路径):①同会话有一条**无调用锚**的历史
|
|
195
|
+
* park 行、而本次决的那只 checkpoint 从未在本账本留过铸造行(headless park / 换过账本,头注明写
|
|
196
|
+
* 这是合法形)⇒ 合并后唯一候选是那条历史行,行会写成「approvalId/taskId = 历史那只、toolCallId =
|
|
197
|
+
* 这一只」的自相矛盾审计行;②精确唯一命中被同会话弱候选拖成假 `ambiguous_anchor`,一个**本可
|
|
198
|
+
* 确定**的身份被降级。弱证据不足以证明「这一次决的就是它」,证不出就不写(步骤 2 同一条纪律)。
|
|
199
|
+
* 2. **late 门**:候选里筛出账本已有 `settle{outcome:"expired", parkRouted:true}` 的那些。
|
|
200
|
+
* 筛空 ⇒ **一行不写**。理由是不编事实:证不出「窗到期已转 durable」,这次决议就只是一次普通的
|
|
201
|
+
* durable decide(headless 提交的 park、SQL 车道搬来的账本、或本店根本没记过这只 ask),
|
|
202
|
+
* 写 `lateAfterExpiry:true` 会把推断当成事实上账 —— 那比缺一行更坏(审计行是要被当证据引的)。
|
|
203
|
+
* 3. **落行**:唯一命中 ⇒ 带真 `approvalId` + 铸造 `taskId`;多条同锚命中 ⇒ `approvalId:null` +
|
|
204
|
+
* `unresolved:"ambiguous_anchor"`(**不静默**:身份说不清照样留行,锚全在行上给人去 join)。
|
|
205
|
+
*
|
|
206
|
+
* 纯观察:自吞一切失败(append 的响亮留痕在 {@link append} 里),不返回受否 —— 结算面绝不因审计写不动
|
|
207
|
+
* 而改变(与 `recordAsk`/`recordSettle` 同姿态,裁 (c) 的行为面红线)。
|
|
208
|
+
*/
|
|
209
|
+
recordLateSettle(input: AuditLateSettleInput): void;
|
|
111
210
|
/**
|
|
112
211
|
* boot 崩溃收敛器(装配层在构造后、协调器开始服务前调一次;幂等跨 boot)。
|
|
113
212
|
* 判据 = 头注「boot 收敛判据」段(一处成文,不复述)。返回两分臂计数 + 坏行数供 boot 日志。
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
4
5
|
export const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
|
|
5
6
|
export const CRASH_CONVERGED_LIST_MAX = 200;
|
|
7
|
+
export function buildCheckpointAnchorHash(token) {
|
|
8
|
+
return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 16);
|
|
9
|
+
}
|
|
6
10
|
function readLedgerRecord(u) {
|
|
7
11
|
if (u === null || typeof u !== "object" || Array.isArray(u))
|
|
8
12
|
return undefined;
|
|
@@ -75,6 +79,43 @@ function readLedgerRecord(u) {
|
|
|
75
79
|
return undefined;
|
|
76
80
|
return { kind: "crash_converged", approvalId, orphanState, convergedAtMs };
|
|
77
81
|
}
|
|
82
|
+
case "late_settle": {
|
|
83
|
+
const idV = o.approvalId;
|
|
84
|
+
const approvalId = typeof idV === "string" ? idV : idV === null ? null : undefined;
|
|
85
|
+
const outcomeV = o.outcome;
|
|
86
|
+
const outcome = outcomeV === "allowed" || outcomeV === "denied" ? outcomeV : undefined;
|
|
87
|
+
const settledBy = s("settledBy");
|
|
88
|
+
const settledAtMs = num("settledAtMs");
|
|
89
|
+
const sessionId = s("sessionId");
|
|
90
|
+
if (approvalId === undefined || outcome === undefined || settledBy === undefined || settledAtMs === undefined || sessionId === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (bool("lateAfterExpiry") !== true)
|
|
93
|
+
return undefined;
|
|
94
|
+
const toolCallId = s("toolCallId");
|
|
95
|
+
const checkpointTokenHash = s("checkpointTokenHash");
|
|
96
|
+
const taskId = s("taskId");
|
|
97
|
+
const unresolvedV = o.unresolved;
|
|
98
|
+
if (unresolvedV !== undefined && unresolvedV !== "ambiguous_anchor")
|
|
99
|
+
return undefined;
|
|
100
|
+
const unresolved = unresolvedV === "ambiguous_anchor" ? "ambiguous_anchor" : undefined;
|
|
101
|
+
if ((approvalId === null) !== (unresolved !== undefined))
|
|
102
|
+
return undefined;
|
|
103
|
+
if (approvalId === null && taskId !== undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
kind: "late_settle",
|
|
107
|
+
approvalId,
|
|
108
|
+
outcome,
|
|
109
|
+
settledBy,
|
|
110
|
+
lateAfterExpiry: true,
|
|
111
|
+
settledAtMs,
|
|
112
|
+
sessionId,
|
|
113
|
+
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
114
|
+
...(checkpointTokenHash !== undefined ? { checkpointTokenHash } : {}),
|
|
115
|
+
...(taskId !== undefined ? { taskId } : {}),
|
|
116
|
+
...(unresolved !== undefined ? { unresolved } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
78
119
|
default:
|
|
79
120
|
return undefined;
|
|
80
121
|
}
|
|
@@ -89,6 +130,10 @@ export class FileApprovalAskAuditStore {
|
|
|
89
130
|
convergedRows = [];
|
|
90
131
|
openLegTasks = new Set();
|
|
91
132
|
convergedOnce = false;
|
|
133
|
+
askByCallAnchor = new Map();
|
|
134
|
+
askBySessionOnly = new Map();
|
|
135
|
+
parkExpired = new Set();
|
|
136
|
+
askTaskId = new Map();
|
|
92
137
|
constructor(root, internals) {
|
|
93
138
|
this.writeBytes = internals?.writeBytes ?? ((fd, buf, off, len) => writeSync(fd, buf, off, len));
|
|
94
139
|
const dir = join(root, APPROVAL_ASK_AUDIT_DIR);
|
|
@@ -133,6 +178,7 @@ export class FileApprovalAskAuditStore {
|
|
|
133
178
|
g.ask = rec;
|
|
134
179
|
g.askLine = line;
|
|
135
180
|
}
|
|
181
|
+
this.indexAsk(rec);
|
|
136
182
|
break;
|
|
137
183
|
}
|
|
138
184
|
case "settle": {
|
|
@@ -141,8 +187,11 @@ export class FileApprovalAskAuditStore {
|
|
|
141
187
|
g.settle = rec;
|
|
142
188
|
g.settleLine = line;
|
|
143
189
|
}
|
|
190
|
+
this.indexSettle(rec);
|
|
144
191
|
break;
|
|
145
192
|
}
|
|
193
|
+
case "late_settle":
|
|
194
|
+
break;
|
|
146
195
|
case "leg_closed":
|
|
147
196
|
this.replayedLegClosed.push({ taskId: rec.taskId, line });
|
|
148
197
|
break;
|
|
@@ -183,19 +232,48 @@ export class FileApprovalAskAuditStore {
|
|
|
183
232
|
while (off < buf.length)
|
|
184
233
|
off += this.writeBytes(this.fd, buf, off, buf.length - off);
|
|
185
234
|
fsyncSync(this.fd);
|
|
235
|
+
return true;
|
|
186
236
|
}
|
|
187
237
|
catch (err) {
|
|
188
238
|
this.tailDirty = true;
|
|
189
239
|
recordFailOpen("server.approval-ask-audit.append-failed", `kind=${rec.kind} err=${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
static callAnchorKey(session, toolCallId) {
|
|
244
|
+
return `${session}\u0000${toolCallId}`;
|
|
245
|
+
}
|
|
246
|
+
indexAsk(rec) {
|
|
247
|
+
this.askTaskId.set(rec.approvalId, rec.taskId);
|
|
248
|
+
const sessions = new Set();
|
|
249
|
+
if (rec.sessionId !== undefined)
|
|
250
|
+
sessions.add(rec.sessionId);
|
|
251
|
+
if (rec.sourceTaskId !== undefined)
|
|
252
|
+
sessions.add(rec.sourceTaskId);
|
|
253
|
+
for (const session of sessions) {
|
|
254
|
+
const bucket = rec.toolCallId !== undefined ? this.askByCallAnchor : this.askBySessionOnly;
|
|
255
|
+
const key = rec.toolCallId !== undefined ? FileApprovalAskAuditStore.callAnchorKey(session, rec.toolCallId) : session;
|
|
256
|
+
let ids = bucket.get(key);
|
|
257
|
+
if (!ids) {
|
|
258
|
+
ids = new Set();
|
|
259
|
+
bucket.set(key, ids);
|
|
260
|
+
}
|
|
261
|
+
ids.add(rec.approvalId);
|
|
190
262
|
}
|
|
191
263
|
}
|
|
264
|
+
indexSettle(rec) {
|
|
265
|
+
if (rec.outcome === "expired" && rec.parkRouted === true)
|
|
266
|
+
this.parkExpired.add(rec.approvalId);
|
|
267
|
+
}
|
|
192
268
|
recordAsk(input) {
|
|
193
269
|
if (input.legTracked)
|
|
194
270
|
this.openLegTasks.add(input.taskId);
|
|
195
|
-
this.append({ kind: "ask", ...input })
|
|
271
|
+
if (this.append({ kind: "ask", ...input }))
|
|
272
|
+
this.indexAsk(input);
|
|
196
273
|
}
|
|
197
274
|
recordSettle(input) {
|
|
198
|
-
this.append({ kind: "settle", ...input })
|
|
275
|
+
if (this.append({ kind: "settle", ...input }))
|
|
276
|
+
this.indexSettle(input);
|
|
199
277
|
}
|
|
200
278
|
noteLegClosed(taskId) {
|
|
201
279
|
if (!this.openLegTasks.has(taskId))
|
|
@@ -203,6 +281,29 @@ export class FileApprovalAskAuditStore {
|
|
|
203
281
|
this.openLegTasks.delete(taskId);
|
|
204
282
|
this.append({ kind: "leg_closed", taskId, closedAtMs: Date.now() });
|
|
205
283
|
}
|
|
284
|
+
recordLateSettle(input) {
|
|
285
|
+
const candidates = input.toolCallId !== undefined
|
|
286
|
+
? (this.askByCallAnchor.get(FileApprovalAskAuditStore.callAnchorKey(input.sessionId, input.toolCallId)) ?? new Set())
|
|
287
|
+
: (this.askBySessionOnly.get(input.sessionId) ?? new Set());
|
|
288
|
+
const parked = [...candidates].filter((id) => this.parkExpired.has(id));
|
|
289
|
+
if (parked.length === 0)
|
|
290
|
+
return;
|
|
291
|
+
const only = parked.length === 1 ? parked[0] : undefined;
|
|
292
|
+
const taskId = only !== undefined ? this.askTaskId.get(only) : undefined;
|
|
293
|
+
this.append({
|
|
294
|
+
kind: "late_settle",
|
|
295
|
+
approvalId: only ?? null,
|
|
296
|
+
outcome: input.outcome,
|
|
297
|
+
settledBy: input.settledBy,
|
|
298
|
+
lateAfterExpiry: true,
|
|
299
|
+
settledAtMs: input.settledAtMs,
|
|
300
|
+
sessionId: input.sessionId,
|
|
301
|
+
...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
|
|
302
|
+
...(input.checkpointTokenHash !== undefined ? { checkpointTokenHash: input.checkpointTokenHash } : {}),
|
|
303
|
+
...(taskId !== undefined ? { taskId } : {}),
|
|
304
|
+
...(only === undefined ? { unresolved: "ambiguous_anchor" } : {}),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
206
307
|
convergeCrashedAsks() {
|
|
207
308
|
if (this.convergedOnce)
|
|
208
309
|
return { pending: 0, decided: 0, inconsistent: 0, corruptLines: this.corruptLines };
|