@sema-agent/server 7.71.0 → 7.73.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 +1 -1
- package/USAGE.md +12 -1
- package/dist/approval-card.d.ts +16 -17
- package/dist/approval-card.js +0 -9
- package/dist/boot/config-center.d.ts +6 -1
- package/dist/boot/config-center.js +3 -1
- package/dist/boot/memory-boundary.d.ts +27 -5
- package/dist/boot/memory-boundary.js +6 -6
- package/dist/boot/memory-consolidation.js +2 -1
- package/dist/boot/runner-deps.d.ts +32 -16
- package/dist/boot/runner-deps.js +3 -1
- package/dist/boot/stores.js +6 -4
- package/dist/boot/unpriced-ceiling-verdict.d.ts +15 -0
- package/dist/boot/unpriced-ceiling-verdict.js +39 -0
- package/dist/brain.js +2 -9
- package/dist/budget.d.ts +21 -2
- package/dist/budget.js +3 -3
- package/dist/capabilities/tool-defer.d.ts +18 -12
- package/dist/config-catalog.js +12 -12
- package/dist/config-center/apply-effective.js +2 -4
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/restart-signal.js +2 -2
- package/dist/config-center/skills-mcp.js +15 -0
- package/dist/config-types.d.ts +83 -23
- package/dist/config-types.js +19 -1
- package/dist/config.d.ts +58 -0
- package/dist/config.js +76 -22
- package/dist/http/active-run-conflict.d.ts +3 -2
- package/dist/http/durable-run-leg.d.ts +64 -0
- package/dist/http/durable-run-leg.js +63 -0
- package/dist/http/route-ctx.d.ts +14 -0
- package/dist/http/route-ctx.js +3 -0
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/runs.js +23 -68
- package/dist/http/routes/workflows.js +1 -0
- package/dist/http/server.js +70 -24
- package/dist/main.js +4 -1
- package/dist/memory-bundle-engine.d.ts +4 -3
- package/dist/memory-bundle-engine.js +2 -1
- package/dist/memory-incident-sink.d.ts +37 -0
- package/dist/memory-incident-sink.js +13 -0
- package/dist/memory-operator-faces.d.ts +9 -3
- package/dist/memory-operator-faces.js +4 -3
- package/dist/memory-scope.d.ts +37 -1
- package/dist/memory-scope.js +12 -2
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/tool-trace.d.ts +18 -1
- package/dist/observability/tool-trace.js +4 -3
- package/dist/parked-decide.js +3 -2
- package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
- package/dist/plugins/e2b-orphan-reclaim.js +18 -20
- package/dist/plugins/file-run-store.js +1 -0
- package/dist/plugins/memory-run-store.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +5 -2
- package/dist/plugins/run-store-sql.js +5 -3
- package/dist/plugins/store-contracts.d.ts +41 -0
- package/dist/plugins/store-contracts.js +3 -2
- package/dist/run-cancel-context.js +1 -1
- package/dist/run-local.js +19 -8
- package/dist/runs.d.ts +16 -2
- package/dist/runs.js +2 -1
- package/dist/runtime-caps-resolver.js +5 -3
- package/dist/spec-fields.d.ts +4 -2
- package/dist/task-a2a.js +3 -2
- package/dist/task-mcp.js +3 -2
- package/dist/tool-approval.d.ts +26 -21
- package/dist/tool-approval.js +2 -2
- package/dist/trace/core-keyset-guard.d.ts +17 -5
- package/dist/trace/project.d.ts +55 -0
- package/dist/trace/project.js +45 -3
- package/dist/usage-analytics.d.ts +20 -2
- package/dist/usage-analytics.js +9 -1
- package/dist/wire-name-bound.d.ts +14 -0
- package/dist/wire-name-bound.js +13 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
164
164
|
| `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 |
|
|
165
165
|
| `WRITE_PROTECTED_EXTRA` | unset (engine default table) | **Adds** rows to core's write-protection table (the literal-name table whose hit demotes a surviving `allow` to `ask` on Write/Edit/NotebookEdit). Comma-separated bare names (a bare name matches ANY path segment; a name containing `/` matches a consecutive segment run) or a JSON array of `"name"` strings / `{name, kind}` rows (`kind`: `basename` \| `segment` \| `segment-run`). The value is composed as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, so **no default row can be lost**. Unset = no seat is wired = the engine's default table is in force (this server never copies that table). Form discrimination is by content, not by first character: a value containing any JSON structural character (`[ ] { } "`) is parsed as JSON and **must** be a top-level array (a missing pair of brackets refuses to start instead of being split into junk bare names). An empty value, a malformed entry (glob metacharacter, unknown kind, kind/name mismatch) or setting this together with `WRITE_PROTECTED_TABLE_REPLACE` **refuses to start**. Read faces: `GET /v1/capabilities` → `writeProtection.{armed,rows,replaced}`; `GET /v1/diagnostics/wiring` → `writeProtection.{rows,source,droppedDefaultRows}` (operator-only) |
|
|
166
166
|
| `WRITE_PROTECTED_TABLE_REPLACE` | unset (engine default table) | **Replaces the whole** write-protection table (core's seat is whole-table by contract). JSON array only — deliberately no comma shorthand, because a slipped bare string would swap 51 default rows for one. `[]` = the explicit "no write-protection table at all" posture. Replacing logs one **loud** boot line naming every default row you dropped (`write_protection_table_replaced`; the empty posture logs `write_protection_table_disabled`) — use `WRITE_PROTECTED_EXTRA` when you meant to ADD. Same refuse-to-start conditions as its sibling, plus: both knobs set = two writers on one surface = refuses to start |
|
|
167
|
-
| `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,
|
|
167
|
+
| `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 → core's `off` default). Since 7.73.0 (core 7.15.0) `off` no longer means "no gate at all": the READ BOUNDARY (built-in read-deny tiers + workspace containment) is judged under **every** doctrine, so on the `off` lane a deny-listed read, an out-of-root read or a recursive read form still raises exactly ONE mandated approval (no stored rule and no auto-mode classifier can clear it); `shellGate` now governs only the RESIDUAL shell risk. Non-readers (`rm`/`curl`/`git`/`npm`) and ordinary in-workspace reads stay unasked on that lane. 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) |
|
|
168
168
|
| `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) |
|
|
169
169
|
| `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 |
|
|
170
170
|
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
|
package/USAGE.md
CHANGED
|
@@ -161,7 +161,8 @@ MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;词
|
|
|
161
161
|
**坏形/坏词/未知键 ⇒ 丢掉这条 `compat` 声明、模型本体保留**,并打一条 `model_compat_dropped`
|
|
162
162
|
点名 warn(引擎回落到自己的推断 = 今天的行为);一条写坏的声明**不会**让这只模型或整套目录消失。
|
|
163
163
|
- 反应式降级 brain 包在**最外层**;fallback brain 用自己的凭据(decorator 清掉主模型的 per-call key 防外泄给别的 provider)。**坑**:`MODEL_DEGRADE_TO` 最好别和被限流的是同一网关/账号,否则反应式切过去照样撞同一个 rate_limit。
|
|
164
|
-
- **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens
|
|
164
|
+
- **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens**)。**四键一个都不设 = 未定价**:成本读面**缺席**(`result.stats.costMicroUsd` 整键不出、`costBreakdown` 不产、`/v1/capabilities.pricingConfigured=false`),不是 `$0` —— 消费端应渲成「未知」。设其中任一键 ⇒ 四键在场(未设的按 0)。本地自托管(qwen)真的没有 per-token 外部花费时,请**显式**把四键设成 `0`:那是一次「声明免费」,读面照常发 `costMicroUsd:0` 且 `pricingConfigured=true`。云模型(如 review-gw 的 deepseek-v4-flash)必须设真价,否则 spend 无从计算。字段名是 `costUsd`,**非美元计价的网关要先折算**(如 DeepSeek 官方 CNY ÷ 汇率)。配置控制面管的模型走 `CenterModel.cost`(中心存价、不存 secret;名册行不带 `cost` = 该模型未定价,同上)。
|
|
165
|
+
- **成本天花板要有价才咬得住**:`MAX_TASK_COST_USD` / `MAX_PRINCIPAL_COST_USD` 量的是 `costUsd`,未定价的模型算不出成本 ⇒ 这两道闸**永不触发**。目录里一只带价模型都没有却配了天花板时,boot 会打一条 `cost_ceiling_without_pricing` 告警(不拒启);token 轴的 `MAX_TASK_TOKENS` 不受影响。
|
|
165
166
|
- 成本计量**自动开**:从 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 降级)。
|
|
166
167
|
- **近预算降级 vs 硬闸**:降级(`MODEL_DEGRADE_TO`,到 `atCostFraction` 切便宜模型)是**撑长**预算、任务仍完成(出口质量下降、发 `task.degraded` 事件可告警);硬闸(`maxCostUsd` 全额)仍在,切了便宜模型还超全额 → `limits.max_cost_exceeded` 停。
|
|
167
168
|
- **`METRICS_TOKEN`**(可选,只读):设了它,`GET /metrics`+`/metrics/summary` 接受**它或** `SERVICE_AUTH_TOKEN`。**全 fleet 设同一个值** → 管理端(sema-web,规划名 sema-admin)用**一个** token 拉所有 worker 的指标,**无需持有各 worker 的全权 `SERVICE_AUTH_TOKEN`**(不破坏 secret 边界)。即使外泄也只暴露指标(只读)。
|
|
@@ -1292,6 +1293,16 @@ tag 词表与每条的最坏后果见 `docs/FAIL-OPEN-CENSUS.md`。引擎装配
|
|
|
1292
1293
|
"bypassPermissions"` 时是调用方要的姿态,降为 **info** 并标 `expected`——只有没表态落到引擎默认 off 的无壳直连
|
|
1293
1294
|
形才 warn。
|
|
1294
1295
|
|
|
1296
|
+
🔴 **`shell-gate-off` 这条注的语义自 7.73.0(core 7.15.0)起收窄,文案由引擎改写**:`off` 档**不再**意味着
|
|
1297
|
+
「这条会话的 shell 完全没有门」。**读边界**(内建读拒表 + 工作区容纳面)现在**每一档都判**,`shellGate` 只管
|
|
1298
|
+
**剩余**风险 ⇒ `off` 车道下一条读拒表路径(`cat .claude/settings.json`)、工作区外路径,或递归读形
|
|
1299
|
+
(`grep -r … .` / `find .` / `ls -R` / `du .`)都会产**恰一个 mandated 审批**(规则与 auto 分类器都清不掉);
|
|
1300
|
+
**非读**命令(`rm` / `curl` / `git` / `npm`)与工作区内的普通读在 `off` 下仍然零审批。注文案里那句
|
|
1301
|
+
「the read boundary is still judged on every command」就是这件事。⇒ **运维侧后果**:一台把 `bypassPermissions`
|
|
1302
|
+
当「无人值守直跑」用的部署,现在会在这三类命令上落审批(无活体席 ⇒ durable park,出现在 `GET /v1/approvals`)。
|
|
1303
|
+
要让它们重新不问,得从**读面**下手(`READ_FACE` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE`,
|
|
1304
|
+
见 `docs/DEPLOY-PREREQS.md`),`permissionMode` 那根杆子够不着它。
|
|
1305
|
+
|
|
1295
1306
|
### 9.1 limits.approachNotice(6.1.0 起)
|
|
1296
1307
|
|
|
1297
1308
|
引擎默认在上下文用量到 80%/95% 时各注入一次接近提示(SSE 上是 `steering_injected` 帧、
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -328,27 +328,26 @@ declare const DenialLimitFallbackSchema: z.ZodObject<{
|
|
|
328
328
|
autoDenyAfterMs: z.ZodNumber;
|
|
329
329
|
}, z.core.$strict>;
|
|
330
330
|
export type DenialLimitFallback = z.infer<typeof DenialLimitFallbackSchema>;
|
|
331
|
-
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
331
|
+
/**
|
|
332
|
+
* ⚰️ **退役成员的形(server 7.69.0–7.71.0 铸过,7.72.0 起永不再铸)** —— core 7.14.0 [ref] 把
|
|
333
|
+
* `classifierUnavailable` 从 ask 侧**整族**删掉(事实改骑 `GateDisposition.denied.cause`),所以本仓
|
|
334
|
+
* 的**铸造**腿(`buildApprovalCard` / 活卡帧 / inbox 行)本批全部摘掉;窄读函数
|
|
335
|
+
* `readClassifierUnavailable` 与对 core 型面的合规钉连同它们的素材一起删(素材没了,钉的两端只剩一端)。
|
|
336
|
+
*
|
|
337
|
+
* 🔴 **这只 schema 本身留着,而且是承重的,不是没删干净**:`ApprovalCardSchema` 是 `.strict()`,
|
|
338
|
+
* 而它同时是 `approval_ask.card_json` 这张**耐久列**的读契约(读侧三个 `safeParse`:重放腿、幂等重入、
|
|
339
|
+
* 对账扫描)。把成员从 strict schema 上删掉 ⇒ 7.69.0–7.71.0 铸下、升级时**仍然 pending** 的那些行
|
|
340
|
+
* 当场判假 ⇒ 「跳过 + 一次 warn」:人手上那张卡在重连后变不回来、幂等重入退成 park。
|
|
341
|
+
* ⇒ 一条耐久投影的 schema **必须读得懂它自己写过的每一版**;写侧只写当前引擎供得出的键,读侧接受
|
|
342
|
+
* 全部历史成员。这不是给退役键开特例,这是 strict + durable 这一组合的内生义务(同样的处置将适用于
|
|
343
|
+
* 未来任何一次键退役)。
|
|
344
|
+
*
|
|
345
|
+
* 形保持原样(单成员 `cause` 必填、`.strict()`):它描述的是**已经写在库里的字节**,不许再动。
|
|
346
|
+
*/
|
|
338
347
|
declare const ClassifierUnavailableSchema: z.ZodObject<{
|
|
339
348
|
cause: z.ZodString;
|
|
340
349
|
}, z.core.$strict>;
|
|
341
350
|
export type ClassifierUnavailable = z.infer<typeof ClassifierUnavailableSchema>;
|
|
342
|
-
/**
|
|
343
|
-
* `AskRequest.classifierUnavailable`(core 7.10.0 [ref])的**边界窄读** —— 活卡帧与 `card_json` 的唯一
|
|
344
|
-
* 铸造点(与 {@link readDenialLimitFallback} 同款分工),两面结构性同值。
|
|
345
|
-
*
|
|
346
|
-
* 🔴 **缺席不是断言**:缺席同时覆盖「分类器答上了」「这只 ask 没资格走分类器」「本部署没接分类器」
|
|
347
|
-
* 三形(core d.ts 逐字:read presence, never absence),消费端禁读成「分类器好着呢」。
|
|
348
|
-
* 🔴 **echo-only**:server 不据它做任何裁决 —— 熔断/超时的处置全在 core 的分类器站,据本键在本仓自铸
|
|
349
|
-
* 第二套回落判据就是同一语义面两个写者(源头修复纪律)。
|
|
350
|
-
*/
|
|
351
|
-
export declare function readClassifierUnavailable(req: unknown): ClassifierUnavailable | undefined;
|
|
352
351
|
/**
|
|
353
352
|
* `AskRequest.denialLimitFallback`(core 7.4.0 [ref])的**边界窄读** —— 活卡帧与 `card_json` 的唯一铸造点
|
|
354
353
|
* (与 {@link readRuleOffersAbsence} / {@link readProbeCause} 同款分工),两面结构性同值。
|
package/dist/approval-card.js
CHANGED
|
@@ -238,13 +238,6 @@ const _denialLimitFallbackConformance = [true, true];
|
|
|
238
238
|
void _denialLimitFallbackConformance;
|
|
239
239
|
const DenialLimitFallbackEnvelopeSchema = z.object({ denialLimitFallback: DenialLimitFallbackSchema.optional() });
|
|
240
240
|
const ClassifierUnavailableSchema = z.object({ cause: z.string().min(1).max(MAX_IDENT) }).strict();
|
|
241
|
-
const _classifierUnavailableConformance = [true];
|
|
242
|
-
void _classifierUnavailableConformance;
|
|
243
|
-
const ClassifierUnavailableEnvelopeSchema = z.object({ classifierUnavailable: ClassifierUnavailableSchema.optional() });
|
|
244
|
-
export function readClassifierUnavailable(req) {
|
|
245
|
-
const parsed = ClassifierUnavailableEnvelopeSchema.safeParse(req);
|
|
246
|
-
return parsed.success ? parsed.data.classifierUnavailable : undefined;
|
|
247
|
-
}
|
|
248
241
|
export function readDenialLimitFallback(req) {
|
|
249
242
|
const parsed = DenialLimitFallbackEnvelopeSchema.safeParse(req);
|
|
250
243
|
return parsed.success ? parsed.data.denialLimitFallback : undefined;
|
|
@@ -312,7 +305,6 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
312
305
|
const ruleEvidence = readRuleEvidence(req);
|
|
313
306
|
const ruleOffersAbsence = readRuleOffersAbsence(req);
|
|
314
307
|
const denialLimitFallback = readDenialLimitFallback(req);
|
|
315
|
-
const classifierUnavailable = readClassifierUnavailable(req);
|
|
316
308
|
const toolCallId = clip(source.toolCallId, MAX_IDENT);
|
|
317
309
|
const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
|
|
318
310
|
const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
|
|
@@ -335,7 +327,6 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
335
327
|
...(source.ruleOffers !== undefined && source.ruleOffers.length > 0 ? { ruleOffers: source.ruleOffers.map(copyRuleOffer) } : {}),
|
|
336
328
|
...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
|
|
337
329
|
...(denialLimitFallback !== undefined ? { denialLimitFallback } : {}),
|
|
338
|
-
...(classifierUnavailable !== undefined ? { classifierUnavailable } : {}),
|
|
339
330
|
...(source.fromSubagent === true ? { fromSubagent: true } : {}),
|
|
340
331
|
...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
|
|
341
332
|
...(sourceAgentName !== undefined ? { sourceAgentName } : {}),
|
|
@@ -7,6 +7,7 @@ import { type ServiceConfig } from "../config.js";
|
|
|
7
7
|
import { type ModelKeyRef } from "../key-resolver.js";
|
|
8
8
|
import type { Logger } from "../observability/logger.js";
|
|
9
9
|
import type { Metrics } from "../observability/metrics.js";
|
|
10
|
+
import type { UnpricedCeilingVerdict } from "./unpriced-ceiling-verdict.js";
|
|
10
11
|
import { type RestartSignal } from "../config-center/facade.js";
|
|
11
12
|
import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
|
|
12
13
|
import type { LimitSync } from "./limit-sync.js";
|
|
@@ -45,12 +46,16 @@ export interface ConfigCenterRuntime {
|
|
|
45
46
|
* `swapRunnerDeps`(core 7.10.0 `Runner.swapDeps` 一扇门;5.49 起的 `swapModels` 口在 7.10.0 退役、
|
|
46
47
|
* 本参数同语义改名)= **可换席**的换装口:模型面 plane 变更候选在 commit 前喂给全部 boot Runner
|
|
47
48
|
* 原子换代(在场 ⇒ models-tiers defer/restart 臂退役,缺席 ⇒ 旧 defer 行为逐字保留);S-174 起
|
|
48
|
-
* read **face** 席也走同一只函数(见 refresh 拍里的 `applyCenterReadFace` 调用点)。
|
|
49
|
+
* read **face** 席也走同一只函数(见 refresh 拍里的 `applyCenterReadFace` 调用点)。
|
|
50
|
+
* `ceilingVerdict`(S-232)= 「配了 $ 天花板但目录未定价」这条判决的活载体,与 `limitSync` **同族同序**
|
|
51
|
+
* (commit 之后、零 await 内重判一次;判决不翻转就静默)。缺席 = 这条腿本部署没装配(run-local 一次性
|
|
52
|
+
* CLI 之后不再有刷新拍,它继续用纯判据判一次)。 */
|
|
49
53
|
startRefreshLoop(a: {
|
|
50
54
|
runnerTierFrozen: boolean;
|
|
51
55
|
pricing: ReturnType<typeof buildPricing>;
|
|
52
56
|
limitSync?: LimitSync;
|
|
53
57
|
swapRunnerDeps?: (next: SwappableDeps) => void;
|
|
58
|
+
ceilingVerdict?: UnpricedCeilingVerdict;
|
|
54
59
|
}): void;
|
|
55
60
|
/** [ref]-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。 */
|
|
56
61
|
stopRefreshLoop(): void;
|
|
@@ -510,7 +510,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
510
510
|
}
|
|
511
511
|
},
|
|
512
512
|
startRefreshLoop(a) {
|
|
513
|
-
const { runnerTierFrozen, pricing, limitSync, swapRunnerDeps } = a;
|
|
513
|
+
const { runnerTierFrozen, pricing, limitSync, swapRunnerDeps, ceilingVerdict } = a;
|
|
514
514
|
if (configProvider) {
|
|
515
515
|
const ccRef = config.configCenter;
|
|
516
516
|
let refreshInFlight = false;
|
|
@@ -594,6 +594,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
594
594
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
595
595
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
596
596
|
limitSync?.sync();
|
|
597
|
+
ceilingVerdict?.judge();
|
|
597
598
|
mcpRevocations.applyEnabledEntries(enabledMcpEntries(r.effective.mcp));
|
|
598
599
|
applyLiveReadFace(r.effective, swapRunnerDeps, "refresh");
|
|
599
600
|
applyLedger.applied(tickGen, r.effective.version);
|
|
@@ -741,6 +742,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
741
742
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
742
743
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
743
744
|
limitSync?.sync();
|
|
745
|
+
ceilingVerdict?.judge();
|
|
744
746
|
applyLiveReadFace(r.effective, swapRunnerDeps, "boot-deferred");
|
|
745
747
|
applyLedger.applied(lateGen, r.effective.version);
|
|
746
748
|
if (planeDeferredLate)
|
|
@@ -9,10 +9,26 @@
|
|
|
9
9
|
* `~/.ai-agent`(或 `AGENT_DATA_DIR`),默认围栏是任务 cwd —— 两者天然不相交:用户说「记住 X」,模型照
|
|
10
10
|
* 提示词去 Write,拿回一个 `path_not_in_root`,而回执早就发出去了。
|
|
11
11
|
*
|
|
12
|
-
* 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount /
|
|
13
|
-
*
|
|
12
|
+
* 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount / 部署方自己在别处开了写通道,
|
|
13
|
+
* 都是真实可行的部署形,而我方在 boot 期看不全。看得全的是「按我们自己接的线,
|
|
14
14
|
* 这条链**结构上**走不通」——那句必须说出来。围栏根算不出来时**不说话**(判不了就闭嘴,别把猜测当告警)。
|
|
15
15
|
*
|
|
16
|
+
* 🔴 **已登记的判据限度(不在本批射程,别把它读成「判据已闭合」)**:② 号 warn 今天唯一算得出围栏根的
|
|
17
|
+
* 腿是 `REMOTE_EXEC` 未设的 in-process 形(`boot/stores.ts` 的 `containmentRoots`),而**恰恰是这条腿
|
|
18
|
+
* 没有文件工具** —— 亲读装的这台 core:`prepare-execution-env.js` 的
|
|
19
|
+
* `handsEnabled = ownedEnv !== undefined || deps.executionEnv !== undefined`,本仓不设 `REMOTE_EXEC` 时
|
|
20
|
+
* `executionEnvFactory` 恒 `undefined` ⇒ `handsEnabled=false` ⇒ `prepareHandsMount` 整个不跑。所以这条
|
|
21
|
+
* warn 描述的失败形(「模型照提示词去 Write,拿回 `path_not_in_root`」)在该腿上并不成立:真相是**一只
|
|
22
|
+
* 文件工具都没有**,而按它给的第一条恢复路径(把根挪进 workspace)会让 warn 闭嘴却仍然写不成。
|
|
23
|
+
* 把判据换成「执行能力 + core 自己的 advertised-writable-dir 准入结局」是一次**新的探测臂**(要读 per-task
|
|
24
|
+
* 的准入结果,而本模块是 boot 期纯函数),按复审停机纪律不在提货批里开;登记在此,免下一位重新发现。
|
|
25
|
+
*
|
|
26
|
+
* 🔴 **不再劝 `additionalDirectories`**(core 7.16.0 [ref] 提货):每个 scope 的家是 `<挂载平面>/<label>`,
|
|
27
|
+
* 平面本身谁的家都不是。交配置根 = 连引擎控制面(`.engine`)一起放行;交平面 = 把每个兄弟 scope 的家
|
|
28
|
+
* (含 org 只读层)一起交给模型;交写家 operator 事前说不出口(label 的真源是引擎注册簿),而 core 的
|
|
29
|
+
* 准入席([ref])本来就自动把写家教给模型。三条路各自要么危险要么多余 ⇒ 整条建议删,只留「挪根」与
|
|
30
|
+
* 「表态 capable=false」两条。
|
|
31
|
+
*
|
|
16
32
|
* ⚠️ 这不是 fail-open 的托词:真正的 fail-closed 归属面在别处(多租户记忆整体 dark、remote lane 默认 dark)。
|
|
17
33
|
* 本模块判的是「已经决定要开记忆」之后的**可达性**,而可达性的证据一半在部署环境里,不在进程里。
|
|
18
34
|
*
|
|
@@ -47,8 +63,8 @@
|
|
|
47
63
|
* 工具就跑在**这台机器**的 fs 上(`hostSemanticsLane`,与 core 的 remote 判决是两根轴,见下方 facts 注),
|
|
48
64
|
* 记忆根也在这台机器上,「执行车道与记忆根共享文件平面」这个 capable=true 所断言的**车道级事实**由
|
|
49
65
|
* 构造成立。剩下的围栏维(记忆根是否落在该次任务的 fs 授权边界内)在 in-process 上**同样存在**而 core
|
|
50
|
-
* 并不 withhold——它由本模块的 ② 号 warn、fs 门的 `path_not_in_root`
|
|
51
|
-
*
|
|
66
|
+
* 并不 withhold——它由本模块的 ② 号 warn、fs 门的 `path_not_in_root` 拒绝、以及 core 准入席自动放行写家
|
|
67
|
+
* ([ref])这套既有处置管,不归 withhold 判据管。所以 host 腿自动 true 与 in-process 的默认行为**同等诚实**。
|
|
52
68
|
* 沙箱腿(e2b/k8s/ssh/local-docker)文件平面在沙箱盘,withhold 是对的,恒不自动。
|
|
53
69
|
*
|
|
54
70
|
* ## 为什么不是「围栏覆盖记忆根」的更严合取
|
|
@@ -69,7 +85,13 @@ export declare function effectiveMemoryPersistenceCapable(config: {
|
|
|
69
85
|
}): boolean | undefined;
|
|
70
86
|
/** boot 期可知的全部事实。每一位都必须由调用点从真配置/真装配结果取,禁在本模块内重算(重算=第二真源)。 */
|
|
71
87
|
export interface MemoryWriteBoundaryFacts {
|
|
72
|
-
/**
|
|
88
|
+
/** 引擎的**挂载平面**根 —— 引擎真正读写条目的那棵树,由 `memoryMountRootOf(backend, root)` 单铸点取
|
|
89
|
+
* (file 腿 = `<配置根>/memory`,两只 SQL 孪生不钉平面 ⇒ 回到配置根)。`undefined` = 引擎未接 ⇒ 本判据
|
|
90
|
+
* 整体不适用。
|
|
91
|
+
* 🔴 **不是配置根**(2026 提货批订正):配置根比挂载平面高一层,拿它判包含会把「记忆目录在围栏里、
|
|
92
|
+
* 配置根不在」这一真实可行的部署形误报成「结构性写不进」——而那台机器上模型其实存得下。
|
|
93
|
+
* core 7.16.0([ref])之后写家还要再深一层(`<平面>/<label>`),但 label 归引擎注册簿,boot 期说不出口;
|
|
94
|
+
* 平面是**包含关系上等价**的那一只(家恒为平面的直接子目录),所以判据停在平面,不猜 label。 */
|
|
73
95
|
memoryRoot: string | undefined;
|
|
74
96
|
/** 执行车道名,只进文案(`config.remoteExec?.provider ?? "in-process"`)。 */
|
|
75
97
|
lane: string;
|
|
@@ -45,12 +45,12 @@ export function buildMemoryWriteBoundaryAudit(facts) {
|
|
|
45
45
|
warnings: [
|
|
46
46
|
{
|
|
47
47
|
tag: "memory_root_outside_write_boundary",
|
|
48
|
-
detail: `the memory engine
|
|
49
|
-
`(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into
|
|
50
|
-
`so a "remember this" turn will be answered with a save the file tools
|
|
51
|
-
`
|
|
52
|
-
`
|
|
53
|
-
`deployment cannot save instead of receipting a write that never lands.`,
|
|
48
|
+
detail: `the memory engine mount plane (${memoryRoot}) is outside every file-tool containment root known at boot ` +
|
|
49
|
+
`(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into the write ` +
|
|
50
|
+
`scope's home under that plane — so a "remember this" turn will be answered with a save the file tools ` +
|
|
51
|
+
`structurally refuse (path_not_in_root). ` +
|
|
52
|
+
`Fix it in one of two ways: ${relocateRoot}, or declare MEMORY_PERSISTENCE_CAPABLE=false so the model is told ` +
|
|
53
|
+
`up front that this deployment cannot save instead of receipting a write that never lands.`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
+
import { memoryMountRootOf } from "../memory-scope.js";
|
|
2
3
|
import { expandTiers, MemoryEngine, readConsolidationDriverRun, resolveMemoryConsolidationDriver, runMemoryConsolidationDriver, } from "@sema-agent/core";
|
|
3
4
|
export function resolveConsolidationDriverSeat(deps) {
|
|
4
5
|
let options;
|
|
@@ -109,7 +110,7 @@ export function buildStopDetailAudit(input) {
|
|
|
109
110
|
export function createMemoryConsolidationFaces(store, input) {
|
|
110
111
|
const engine = new MemoryEngine({
|
|
111
112
|
backend: store.backend,
|
|
112
|
-
memoryDir: store.root,
|
|
113
|
+
memoryDir: memoryMountRootOf(store.backend, store.root),
|
|
113
114
|
consolidation: {},
|
|
114
115
|
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
115
116
|
...(input.onIncident !== undefined ? { onIncident: input.onIncident } : {}),
|
|
@@ -84,26 +84,42 @@ export interface RunnerDepsCtx {
|
|
|
84
84
|
*
|
|
85
85
|
* 本键从「差异键」升进{@link createSharedRunnerDeps 共享基座}。修前它**只**挂在 subRunner 上,主
|
|
86
86
|
* runner 靠 per-task `spec.checkpointStore` 拿店 —— 而 core 的 `run_workflow` 是挂在**主** Runner
|
|
87
|
-
* (`prepare-caps-and-workflow.js:
|
|
88
|
-
* :249 是 7.11.2 的坐标
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `
|
|
93
|
-
*
|
|
87
|
+
* (`prepare-caps-and-workflow.js:279` 的 `runner: runnerSelf`;**core 7.14.0 亲核行号**,7.12.0 时是
|
|
88
|
+
* :244、B-060 票据引的 :249 是 7.11.2 的坐标 —— 这一格每版都在漂,引用前必须对着装树重读)上的。
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ **B-060 当初写下的病理有一半已被 core 7.14.0 消解,如实订正**([ref] 的 `CheckpointSeat` 重构):
|
|
91
|
+
* · **7.13.0 及以前**:`orchestration/workflow-primitives.js` 只在父侧
|
|
92
|
+
* `parentCheckpointStoreDisabled === true` 时给子代盖 `"disabled"`,**不把父侧真店透传给子代**
|
|
93
|
+
* ⇒ 子代的 `resolveCheckpointStore(spec, deps)` 落到主 runner 这一格 = `undefined`
|
|
94
|
+
* ⇒ `parkLane {capable:false, reasons:["no_checkpoint_store"]}` ⇒ 受门的调用不是 park 而是
|
|
95
|
+
* `delegation.ask_unresolvable{parkLaneExisted:false}` 直接拒(过度拒绝)。
|
|
96
|
+
* · **7.14.0 起**:core 在 prepare 一次铸出 `checkpointSeat = checkpointSeatOf(spec, resolved)`
|
|
97
|
+
* (`prepare-task.js:262`),两条委派车道**逐字复制**它到子代 —— workflow 腿经
|
|
98
|
+
* `parentCheckpointStore: checkpointSeat`(`prepare-caps-and-workflow.js:335`)落到
|
|
99
|
+
* `workflow-primitives.js:80`(现在的条件是「父侧有座 ∧(座是 disabled ∨ 子 spec 没自带店)」⇒
|
|
100
|
+
* **真店也透传**),Task 腿经 `ToolExecuteContext.checkpointStoreForChildren`。
|
|
101
|
+
* ⇒ 「子代拿不到店」这条病路 core 侧已经堵死。**但本席仍然承重**,理由是那只座**本身**就从
|
|
102
|
+
* `resolveCheckpointStore(spec, deps)` 来:主 runner 这一格空着、而 spec 又没带店时,座就是
|
|
103
|
+
* `undefined`,子代照样什么都拿不到。本席不是 core 单源的第二个写者,它就是 core 读的那个 deps 格。
|
|
94
104
|
*
|
|
95
105
|
* 🔴 host 腿的读数判据是**core 侧硬的**,不是本仓的但愿:`resolveCheckpointStore`
|
|
96
|
-
* (`checkpoint-store.js:
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
106
|
+
* (`checkpoint-store.js:370`,7.14.0 亲核)**spec 优先**——`spec.checkpointStore` 在场就用它、
|
|
107
|
+
* `"disabled"` 就是关、只有缺席才落 deps。而本仓 host 腿恒经 `resolveSpec` 的 durableEnabled 分支盖上
|
|
108
|
+
* **同一个实例**,且 `durableEnabled ⇔ checkpointStore !== undefined`(`boot/coordinators.ts:70` 与
|
|
109
|
+
* main.ts 的构造条件同源)⇒ 每一条经 `resolveSpec` 的腿看到的店逐字同一只。
|
|
110
|
+
*
|
|
111
|
+
* 🔴 **core 7.14.0 新加的响亮拒(装配纪律,本仓已合规但必须知道)**:`agents/subagent.js` 在
|
|
112
|
+
* `wantsBackground` 臂上会检查「`ScenarioDeps.checkpointStore`(→ `bg.checkpointStore`)与本 run 的座
|
|
113
|
+
* **不是同一只**」⇒ `configError("config.invalid_checkpoint_store")` **抛**。本仓 main.ts 的
|
|
114
|
+
* `scenarioDeps.checkpointStore` 与本席是同一个 `const`,run-local 两边都不接 ⇒ 两条腿都不触发;
|
|
115
|
+
* 哪天有人给这两处接了不同的店,失败是响亮的而不是静默半场。
|
|
100
116
|
*
|
|
101
117
|
* ⚠️ **如实登记一处不经解析器的直读**(codex 交叉复审提名,亲读 core dist 确认):
|
|
102
|
-
* `core/runner/run-terminal-adoption.js:
|
|
103
|
-
* `
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
118
|
+
* `core/runner/run-terminal-adoption.js:80`(7.14.0 亲核;7.13.0 时是 :74)读的是
|
|
119
|
+
* `runner.deps.checkpointStore` **本身**,不走 `resolveCheckpointStore`,**也不走座** —— 所以 7.14.0
|
|
120
|
+
* 的座重构对它一个字节都没改,本席对它仍是唯一来源。它是「宿主任务 park 时,把没排空的 steer /
|
|
121
|
+
* follow-up 迁移到 park token 上」那一步 —— 修前主 runner 这一格是空的,于是**整段迁移静默不跑**
|
|
122
|
+
* (那些输入随本腿一起丢,只留 `task.user_followup_undrained` 一类通告)。接上之后它开始真的跑。
|
|
107
123
|
*
|
|
108
124
|
* 🔴 这一席是**部署腿级**的答案,不是「哪只 Runner」的属性:一条腿要么有赎回口(HTTP 面的
|
|
109
125
|
* `/v1/approvals/:id/decide` + resume 家族)、于是它的每只 Runner 都该有店;要么没有(`run-local`
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createFileWorkflowScriptStore } from "@sema-agent/core";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
|
|
4
4
|
import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
|
|
5
|
+
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
5
6
|
import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
|
|
6
7
|
import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
|
|
7
8
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
@@ -91,6 +92,7 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
91
92
|
}
|
|
92
93
|
export function createRunnerDeps(ctx) {
|
|
93
94
|
const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, legacyRewindBoundaryProbe, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
|
|
95
|
+
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
94
96
|
const sharedRunnerDeps = createSharedRunnerDeps(ctx);
|
|
95
97
|
const workflowModelAllowlist = workflowModelAllowlistFor(config);
|
|
96
98
|
const selfOrchestrationDeps = config.selfOrchestrationEnabled
|
|
@@ -207,7 +209,7 @@ export function createRunnerDeps(ctx) {
|
|
|
207
209
|
return;
|
|
208
210
|
}
|
|
209
211
|
if (ctx.phase === "memory") {
|
|
210
|
-
|
|
212
|
+
memoryEngineIncident(err, ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : undefined);
|
|
211
213
|
return;
|
|
212
214
|
}
|
|
213
215
|
if (ctx.phase === "mcp") {
|
package/dist/boot/stores.js
CHANGED
|
@@ -20,7 +20,8 @@ import { ensurePgTaskListSchema, ensureTiDBTaskListSchema } from "../plugins/tas
|
|
|
20
20
|
import { assertDeviceAuditRebindEventSchema, ensurePgDeviceSchema, ensureTiDBDeviceSchema } from "../plugins/device-store-sql.js";
|
|
21
21
|
import { mysqlDriver, pgDriver } from "../plugins/sql-driver.js";
|
|
22
22
|
import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "../plugins/store-backend.js";
|
|
23
|
-
import {
|
|
23
|
+
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
24
|
+
import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLanePosture, memoryEngineRootFor, memoryMountRootOf } from "../memory-scope.js";
|
|
24
25
|
import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
|
|
25
26
|
import { assertPermissionRuleApprovalSchema, assertPermissionRuleTicketSchema } from "../plugins/permission-rule-store-sql.js";
|
|
26
27
|
import { assertApprovalAskRuleMaterialSchema } from "../plugins/approval-ask-store-sql.js";
|
|
@@ -193,7 +194,7 @@ export async function openStores(ctx) {
|
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
196
|
else {
|
|
196
|
-
memoryEngine = memoryEngineBackendFor(config);
|
|
197
|
+
memoryEngine = memoryEngineBackendFor(config, undefined, createMemoryEngineIncidentSink({ logger, metrics }));
|
|
197
198
|
if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
|
|
198
199
|
logger.warn("memory_engine_dir_defaulted", {
|
|
199
200
|
root: memoryEngine.root,
|
|
@@ -206,8 +207,9 @@ export async function openStores(ctx) {
|
|
|
206
207
|
const hostSemanticsLane = lane === undefined || lane === "host";
|
|
207
208
|
const coreRemoteExecutionEnv = config.remoteExec !== undefined;
|
|
208
209
|
const containmentRoots = lane === undefined ? [process.cwd()] : [];
|
|
210
|
+
const memoryMountRoot = memoryEngine ? memoryMountRootOf(memoryEngine.backend, memoryEngine.root) : undefined;
|
|
209
211
|
const audit = buildMemoryWriteBoundaryAudit({
|
|
210
|
-
memoryRoot:
|
|
212
|
+
memoryRoot: memoryMountRoot,
|
|
211
213
|
lane: lane ?? "in-process",
|
|
212
214
|
hostSemanticsLane,
|
|
213
215
|
coreRemoteExecutionEnv,
|
|
@@ -216,7 +218,7 @@ export async function openStores(ctx) {
|
|
|
216
218
|
engineBackend: config.memoryEngineBackend,
|
|
217
219
|
});
|
|
218
220
|
for (const w of audit.warnings)
|
|
219
|
-
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root:
|
|
221
|
+
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root: memoryMountRoot ?? null });
|
|
220
222
|
if (config.memoryPersistenceCapable === undefined && effectiveMemoryPersistenceCapable(config) === true) {
|
|
221
223
|
logger.info?.("memory_persistence_capable_auto_declared", {
|
|
222
224
|
lane: lane ?? "in-process",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
2
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
|
+
export interface UnpricedCeilingVerdictCtx {
|
|
4
|
+
/** 活配置(config-center 热应用就地改写的**同一个**对象——所以每次 `judge()` 读到的是当代值)。 */
|
|
5
|
+
readonly config: Pick<ServiceConfig, "models" | "maxTaskCostUsd" | "maxPrincipalCostUsd">;
|
|
6
|
+
readonly logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
export interface UnpricedCeilingVerdict {
|
|
9
|
+
/** 按**当下**的目录与限额重判一次。判决与上一次相同 ⇒ 静默;翻成「有」⇒ 一条 warn;翻成「无」⇒
|
|
10
|
+
* 一条 info `cost_ceiling_warning_cleared{reason}`(告警为什么不再成立,是运维要看见的事实;静默回落
|
|
11
|
+
* 读不出来,而不带原因的「恢复」在护栏被撤光时是假话)。幂等、零抛。 */
|
|
12
|
+
judge(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createUnpricedCeilingVerdict(ctx: UnpricedCeilingVerdictCtx): UnpricedCeilingVerdict;
|
|
15
|
+
//# sourceMappingURL=unpriced-ceiling-verdict.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildUnpricedCeilingNotice, costCeilingPricingPosture, unpricedCeilingClearReasonOf } from "../config.js";
|
|
2
|
+
function verdictKey(notice) {
|
|
3
|
+
return notice?.fields.ceilings;
|
|
4
|
+
}
|
|
5
|
+
export function createUnpricedCeilingVerdict(ctx) {
|
|
6
|
+
const { config, logger } = ctx;
|
|
7
|
+
let last;
|
|
8
|
+
let judged = false;
|
|
9
|
+
return {
|
|
10
|
+
judge() {
|
|
11
|
+
const notice = buildUnpricedCeilingNotice(config.models, config);
|
|
12
|
+
const key = verdictKey(notice);
|
|
13
|
+
const first = !judged;
|
|
14
|
+
judged = true;
|
|
15
|
+
if (!first && key === last)
|
|
16
|
+
return;
|
|
17
|
+
last = key;
|
|
18
|
+
if (notice) {
|
|
19
|
+
logger?.warn?.(notice.event, notice.fields);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (first)
|
|
23
|
+
return;
|
|
24
|
+
const reason = unpricedCeilingClearReasonOf(costCeilingPricingPosture(config.models, config));
|
|
25
|
+
if (reason === undefined)
|
|
26
|
+
throw new Error("unpriced-ceiling verdict: notice absent but no clear reason — judge and posture disagree");
|
|
27
|
+
logger?.info?.("cost_ceiling_warning_cleared", { reason, note: CLEAR_NOTE[reason] });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const CLEAR_NOTE = {
|
|
32
|
+
no_ceiling: "every $ ceiling (MAX_TASK_COST_USD / MAX_PRINCIPAL_COST_USD) is now unset — the earlier cost_ceiling_without_pricing " +
|
|
33
|
+
"warning no longer applies, but note this deployment has NO $ cost guardrail (by configuration, not by omission).",
|
|
34
|
+
empty_catalog: "the effective catalog is empty — there is nothing to price, so the earlier cost_ceiling_without_pricing warning is " +
|
|
35
|
+
"moot until models arrive; the $ ceilings still cannot fire.",
|
|
36
|
+
priced: "the effective catalog declares a price table again — the $ ceilings (MAX_TASK_COST_USD / MAX_PRINCIPAL_COST_USD) " +
|
|
37
|
+
"can fire once more. This reverses the earlier cost_ceiling_without_pricing warning.",
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=unpriced-ceiling-verdict.js.map
|
package/dist/brain.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, hasAuthCarrier, resolveRouteCredential, routeRefusalText, } from "@sema-agent/core";
|
|
2
2
|
import { normalizeBaseUrl } from "@sema-agent/core";
|
|
3
|
-
import {
|
|
3
|
+
import { buildDefaultResilience } from "./config-types.js";
|
|
4
4
|
import { resolveModelApiKey } from "./key-resolver.js";
|
|
5
5
|
function isOffDeploymentRoot(baseUrl, config) {
|
|
6
6
|
if (!baseUrl)
|
|
@@ -15,14 +15,7 @@ function degradeDeclaredRoot(fallbackBaseUrl, config) {
|
|
|
15
15
|
return armed ? config.gatewayBaseUrl : (fallbackBaseUrl || config.gatewayBaseUrl);
|
|
16
16
|
}
|
|
17
17
|
export const FAST_FAIL_MAX_RETRIES = 2;
|
|
18
|
-
const DEFAULT_RESILIENCE =
|
|
19
|
-
connectTimeoutMs: 30_000,
|
|
20
|
-
firstTokenTimeoutMs: MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT,
|
|
21
|
-
idleTimeoutMs: 300_000,
|
|
22
|
-
circuitBreaker: false,
|
|
23
|
-
failureThreshold: 5,
|
|
24
|
-
cooldownMs: 30_000,
|
|
25
|
-
};
|
|
18
|
+
const DEFAULT_RESILIENCE = buildDefaultResilience();
|
|
26
19
|
export function createBrain(config, deps = {}) {
|
|
27
20
|
const { fetchImpl, breakerState } = deps;
|
|
28
21
|
const r = config.resilience ?? DEFAULT_RESILIENCE;
|
package/dist/budget.d.ts
CHANGED
|
@@ -11,7 +11,15 @@ import type { QuotaTracker } from "./observability/cost-quota.js";
|
|
|
11
11
|
import type { FleetUsageAccumulator } from "./fleet-client.js";
|
|
12
12
|
import type { FleetLeaseManager } from "./fleet-lease.js";
|
|
13
13
|
import { type PromptManifestTracker } from "./observability/prompt-manifest.js";
|
|
14
|
-
/** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent).
|
|
14
|
+
/** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent).
|
|
15
|
+
* B-089:`m.cost` 运行期可缺席(未定价的模型不铸这个键 —— `config.ts` 的 `buildModelCostKey`),于是
|
|
16
|
+
* 未定价模型在本表里**没有行**;core 的 `isModelPriced` 两边都读(`pricing[id]` 或 `model.cost`),
|
|
17
|
+
* 两边同时缺席 = 这只模型未定价 ⇒ 成本读面整键缺席,而不是编造的 0。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 S-231 —— 建行判据就是 core 那只谓词本身,不再同形手抄。第二参传 `undefined` 是**语义**不是省事:
|
|
20
|
+
* 这张表正是本函数在建,建之前它不存在,于是谓词落在它的另一半「模型自报价了吗」上。表建好之后
|
|
21
|
+
* 「本部署配没配价目表」这一问归 {@link isPricingConfigured}(它数的是行;每一行都只由本谓词放进来,
|
|
22
|
+
* 所以两问不可能漂成两个答案)。 */
|
|
15
23
|
export declare function buildPricing(models: Record<string, Model>): Record<string, ModelPricing>;
|
|
16
24
|
/**
|
|
17
25
|
* 缝合审 M3:`capabilities.pricingConfigured` 的判据 —— 提成函数放在 pricing 自己的文件里,因为
|
|
@@ -23,7 +31,18 @@ export declare function buildPricing(models: Record<string, Model>): Record<stri
|
|
|
23
31
|
*
|
|
24
32
|
* 同一条论证在同一个字面量里**已经执行过一次**:紧邻的 `restartState: () => configCenter.restartState()`
|
|
25
33
|
* 带着注释「A live getter (not a snapshot) so /health always reads current」—— 兄弟键漏了。本仓第 7 例同形。
|
|
26
|
-
|
|
34
|
+
*
|
|
35
|
+
* 🔴 B-089(判据本身,同一条「0 不是缺席」的病):此前判的是「**有没有一项正价**」
|
|
36
|
+
* (`some(v > 0)`),于是一台把四项单价**显式**配成 0 的部署(= 运营者声明这个模型免费)被报成
|
|
37
|
+
* 「没配价目表」—— 正好是本键要消歧的两态被折成同一个 false,消费端把一个可信的 $0 渲成 unknown。
|
|
38
|
+
* 判据改为**表在不在**:与 core [ref] 的 `isModelPriced`(「price table 是否 EXIST」)同向,也与
|
|
39
|
+
* 本键在 wire 契约里的原话(「价目表是否已配」)一致。
|
|
40
|
+
* 三态读回:未定价 ⇒ 空表 ⇒ false(与改前同值);声明免费 ⇒ 有行(全零)⇒ true(**唯一变的一格**);
|
|
41
|
+
* 配了真价 ⇒ true(与改前同值)。
|
|
42
|
+
*
|
|
43
|
+
* 🔴 S-231 —— 本函数**不是** `isModelPriced` 的第二份实现,它问的是**部署级**的另一问:「这张表里有行吗」。
|
|
44
|
+
* 「某一只模型定价了吗」只有 core 那一只谓词回答,而这张表的每一行都只由它放进来(见 {@link buildPricing}),
|
|
45
|
+
* 所以两问结构上不可能漂成两个答案。 */
|
|
27
46
|
export declare function isPricingConfigured(pricing: Record<string, ModelPricing>): boolean;
|
|
28
47
|
/**
|
|
29
48
|
* Bound the cardinality of a free-form metric label value (BL-11/BL-21). A label fed by untrusted/unbounded
|
package/dist/budget.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { modelCostToPricing } from "@sema-agent/core";
|
|
1
|
+
import { isModelPriced, modelCostToPricing } from "@sema-agent/core";
|
|
2
2
|
import { currentPrincipal } from "./observability/principal-context.js";
|
|
3
3
|
import { isPermissionRuleEventKind, permissionRuleEventWordOf } from "./observability/permission-rule-events.js";
|
|
4
4
|
import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
|
|
5
5
|
export function buildPricing(models) {
|
|
6
6
|
const pricing = {};
|
|
7
7
|
for (const m of Object.values(models))
|
|
8
|
-
if (m
|
|
8
|
+
if (isModelPriced(m, undefined))
|
|
9
9
|
pricing[m.id] = modelCostToPricing(m.cost);
|
|
10
10
|
return pricing;
|
|
11
11
|
}
|
|
12
12
|
export function isPricingConfigured(pricing) {
|
|
13
|
-
return Object.
|
|
13
|
+
return Object.keys(pricing).length > 0;
|
|
14
14
|
}
|
|
15
15
|
export function cardinalityGuard(cap, onOverflow) {
|
|
16
16
|
const seen = new Set();
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref]④ defer face — EXPERIMENTAL long-tail per-tool defer (default OFF, `TOOL_DEFER_LONGTAIL=true`).
|
|
3
3
|
*
|
|
4
|
-
* Mechanics
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Mechanics (core 7.15.0 `core/runner/tool-defer-gate.ts`, 亲读装树 dist —— 规则从八条收成两条):
|
|
5
|
+
* - 候选**只可能**是 caller spec 工具(scenario/user tools)、远端协议铸名(MCP / A2A)与内置自声明
|
|
6
|
+
* defer 的那对;core 一方名册(Bash/Read/Edit/Grep/Glob/Write/…)结构上永不进候选,所以 CC「12 只
|
|
7
|
+
* 一方工具内联」的姿态**零 server 动作**即成立;
|
|
8
8
|
* ⚠️ **这句在中间那些世代上一度不成立**([ref] 件⑤ 对表):`deferMode:"auto"` 的压力阀曾把内建 fs 族
|
|
9
9
|
* 也卷进候选,小窗模型上首用即激活错;core 5.40.0([ref] a 案,判据帖 [ref] 翻面清单 #3)把阀候选
|
|
10
10
|
* 收回 caller specs,**内建恒 inline**,于是这句话重新为真。**对本仓零行为影响**——理由是下一条:
|
|
11
11
|
* 我们从不开 auto。这一段留着是因为它是那次漂移的出处(数字对了而理由没对,是最难查的一类)。
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* - 🔴 **候选 ≠ 延迟**(core 7.15.0 [ref] 的行为面收窄,本仓两条真后果见下):候选集整体受**一道**
|
|
13
|
+
* 阈值门,全或无 —— 候选的内联字节(name+description+schema)≥ `floor(上下文 token × 10%) × 3`
|
|
14
|
+
* 才延迟,否则**全内联且不挂 `ToolSearch`**。远端协议铸名因此**不再无条件占位**:本仓缺省窗
|
|
15
|
+
* (`MODEL_CONTEXT_WINDOW=262144`)下阈值 = 78 642 字符,一个中小 MCP 名册够不着 ⇒ 首请求带全 schema。
|
|
16
|
+
* 唯一不受门的是调用方的 `TaskSpec.deferTools` 字面名单(按字面恒延迟、不计量)。
|
|
17
|
+
* - `RunnerDeps.deferMode:"auto"` 不是第二道门,而是把候选集**拓宽**到全部 caller spec 后走同一道门 ——
|
|
18
|
+
* 在本仓缺省场景下那会连 Agent 与 Task 四件一起卷进候选(与 CC 姿态相反),所以我们**刻意不开**;
|
|
19
|
+
* 🔴 该判断在 core 5.40.0 / 7.15.0 上**逐字仍成立**(亲验):两次收窄动的都是「候选集是谁」,而
|
|
20
|
+
* Agent / Task 四件是 **caller spec 工具**,收窄后照样在候选池里 —— 不开 auto 的理由没有被消解。
|
|
19
21
|
* 我方**从不接线**这个座:全树零 `RunnerDeps.deferMode` 赋值(`deferModelPlane` 是 config-center 的
|
|
20
22
|
* 同形近名,与本轴无关)。机器钉见 test/tool-defer.test.ts 的 [ref] 件⑤ 格 —— 这条「零接线」正是本仓
|
|
21
23
|
* 在两个 core 世代之间行为逐字不变的**全部**依据,所以它值一根钉而不只是一句注。
|
|
22
|
-
* - per-tool `ToolSpec.defer:true`
|
|
24
|
+
* - per-tool `ToolSpec.defer:true` 是本 helper 施加的那一位。**7.15.0 起它是一次「进候选集」的声明,
|
|
25
|
+
* 不再是「一定占位」的保证** —— 长尾集单独一只(下面实测 ≈842 字符)远在阈值之下,于是这根旋钮在
|
|
26
|
+
* 没有 MCP/A2A 名册的部署上是个 no-op;有名册时它把自己的字节并进同一张账,帮助(或不帮助)开门。
|
|
27
|
+
* ⚠️ 不要为了「恢复旧效果」把它改挂 `TaskSpec.deferTools`:那是调用方的轴,由下游改写上游刚裁定的
|
|
28
|
+
* 语义,属于跨仓纪律明禁的旁路;要改口径请回 core 提。
|
|
23
29
|
*
|
|
24
30
|
* Measured inline surface (chars = description + JSON schema; ÷4 ≈ tokens), default scenario @ core 1.289:
|
|
25
31
|
* Agent 5400 · TaskUpdate 3286 · TaskCreate 2661 · SendUserFile 1282 · TaskList 1075 · SelectEnvironment 842 ·
|