@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.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/USAGE.md +12 -1
  3. package/dist/approval-card.d.ts +16 -17
  4. package/dist/approval-card.js +0 -9
  5. package/dist/boot/config-center.d.ts +6 -1
  6. package/dist/boot/config-center.js +3 -1
  7. package/dist/boot/memory-boundary.d.ts +27 -5
  8. package/dist/boot/memory-boundary.js +6 -6
  9. package/dist/boot/memory-consolidation.js +2 -1
  10. package/dist/boot/runner-deps.d.ts +32 -16
  11. package/dist/boot/runner-deps.js +3 -1
  12. package/dist/boot/stores.js +6 -4
  13. package/dist/boot/unpriced-ceiling-verdict.d.ts +15 -0
  14. package/dist/boot/unpriced-ceiling-verdict.js +39 -0
  15. package/dist/brain.js +2 -9
  16. package/dist/budget.d.ts +21 -2
  17. package/dist/budget.js +3 -3
  18. package/dist/capabilities/tool-defer.d.ts +18 -12
  19. package/dist/config-catalog.js +12 -12
  20. package/dist/config-center/apply-effective.js +2 -4
  21. package/dist/config-center/facade.d.ts +1 -1
  22. package/dist/config-center/restart-signal.js +2 -2
  23. package/dist/config-center/skills-mcp.js +15 -0
  24. package/dist/config-types.d.ts +83 -23
  25. package/dist/config-types.js +19 -1
  26. package/dist/config.d.ts +58 -0
  27. package/dist/config.js +76 -22
  28. package/dist/http/active-run-conflict.d.ts +3 -2
  29. package/dist/http/durable-run-leg.d.ts +64 -0
  30. package/dist/http/durable-run-leg.js +63 -0
  31. package/dist/http/route-ctx.d.ts +14 -0
  32. package/dist/http/route-ctx.js +3 -0
  33. package/dist/http/routes/approvals-assistant.js +3 -3
  34. package/dist/http/routes/capabilities.js +2 -2
  35. package/dist/http/routes/runs.js +23 -68
  36. package/dist/http/routes/workflows.js +1 -0
  37. package/dist/http/server.js +70 -24
  38. package/dist/main.js +4 -1
  39. package/dist/memory-bundle-engine.d.ts +4 -3
  40. package/dist/memory-bundle-engine.js +2 -1
  41. package/dist/memory-incident-sink.d.ts +37 -0
  42. package/dist/memory-incident-sink.js +13 -0
  43. package/dist/memory-operator-faces.d.ts +9 -3
  44. package/dist/memory-operator-faces.js +4 -3
  45. package/dist/memory-scope.d.ts +37 -1
  46. package/dist/memory-scope.js +12 -2
  47. package/dist/observability/fail-open.d.ts +1 -1
  48. package/dist/observability/fail-open.js +1 -1
  49. package/dist/observability/metrics.js +1 -1
  50. package/dist/observability/tool-trace.d.ts +18 -1
  51. package/dist/observability/tool-trace.js +4 -3
  52. package/dist/parked-decide.js +3 -2
  53. package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
  54. package/dist/plugins/e2b-orphan-reclaim.js +18 -20
  55. package/dist/plugins/file-run-store.js +1 -0
  56. package/dist/plugins/memory-run-store.js +1 -0
  57. package/dist/plugins/run-store-sql.d.ts +5 -2
  58. package/dist/plugins/run-store-sql.js +5 -3
  59. package/dist/plugins/store-contracts.d.ts +41 -0
  60. package/dist/plugins/store-contracts.js +3 -2
  61. package/dist/run-cancel-context.js +1 -1
  62. package/dist/run-local.js +19 -8
  63. package/dist/runs.d.ts +16 -2
  64. package/dist/runs.js +2 -1
  65. package/dist/runtime-caps-resolver.js +5 -3
  66. package/dist/spec-fields.d.ts +4 -2
  67. package/dist/task-a2a.js +3 -2
  68. package/dist/task-mcp.js +3 -2
  69. package/dist/tool-approval.d.ts +26 -21
  70. package/dist/tool-approval.js +2 -2
  71. package/dist/trace/core-keyset-guard.d.ts +17 -5
  72. package/dist/trace/project.d.ts +55 -0
  73. package/dist/trace/project.js +45 -3
  74. package/dist/usage-analytics.d.ts +20 -2
  75. package/dist/usage-analytics.js +9 -1
  76. package/dist/wire-name-bound.d.ts +14 -0
  77. package/dist/wire-name-bound.js +13 -0
  78. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
2
  import { THINKING_FORMATS } from "./model-compat.js";
3
- import { MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT } from "./config-types.js";
3
+ import { RESILIENCE_DEFAULTS } from "./config-types.js";
4
4
  import { webSearchConfigFromEnv } from "./plugins/web-search.js";
5
5
  import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
6
6
  export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
@@ -246,8 +246,8 @@ export const CONFIG_CATALOG = [
246
246
  r("MODEL_API_KEY", "modelPlane", "secret", "网关腿共享 API key(per-model 覆写在 center 侧)", { danger: [...SEC, ...BILL] }),
247
247
  r("MODEL_AUTO_COMPACT_TOKENS", "modelPlane", "number", "主槽 dual-window 触发阈(1M 窗不再隐式 967000,缺席则用普通触发几何)", { derivedDefaultNote: "缺席 ⇒ 不铸键(core W-33000 几何)" }),
248
248
  r("MODEL_CASCADE_LADDER", "modelPlane", "csv", "质量门级联梯(目录名 cheapest→strongest;opt-in per-task cascade:true)", { danger: BILL, configKey: "cascadeLadder" }),
249
- r("MODEL_CB_COOLDOWN_MS", "modelPlane", "number", "熔断冷却窗(ms)", { staticDefault: "30000" }),
250
- r("MODEL_CB_FAILURE_THRESHOLD", "modelPlane", "number", "熔断失败阈", { staticDefault: "5" }),
249
+ r("MODEL_CB_COOLDOWN_MS", "modelPlane", "number", "熔断冷却窗(ms)", { staticDefault: String(RESILIENCE_DEFAULTS.cooldownMs.value) }),
250
+ r("MODEL_CB_FAILURE_THRESHOLD", "modelPlane", "number", "熔断失败阈", { staticDefault: String(RESILIENCE_DEFAULTS.failureThreshold.value) }),
251
251
  r("MODEL_CHARS_PER_TOKEN", "modelPlane", "number", "结构估 token 系数(CJK 密集模型建议 2-3;坏值回 core 默认)", { derivedDefaultNote: "缺席 ⇒ core 默认 4" }),
252
252
  r("MODEL_CHEAP_AUTO_COMPACT_TOKENS", "modelPlane", "number", "cheap 槽 dual-window 触发阈(主槽值刻意不外溢)", { derivedDefaultNote: "缺席 ⇒ 不铸键" }),
253
253
  r("MODEL_CHEAP_CHARS_PER_TOKEN", "modelPlane", "number", "cheap 槽结构估 token 系数", { derivedDefaultNote: "缺席 ⇒ 继承主模型" }),
@@ -260,14 +260,14 @@ export const CONFIG_CATALOG = [
260
260
  r("MODEL_CHEAP_MAX_TOKENS", "modelPlane", "number", "cheap 槽输出上限", { derivedDefaultNote: "缺席 ⇒ 继承主模型", center: cModels("models[].maxTokens") }),
261
261
  r("MODEL_CHEAP_REASONING", "modelPlane", "boolean", "cheap 槽 reasoning 声明(三态:缺席继承主模型)", { derivedDefaultNote: "缺席 ⇒ 继承主模型" }),
262
262
  r("MODEL_CHEAP_VISION", "modelPlane", "boolean", "cheap 槽 vision 声明(三态:缺席继承主模型;text-only cheap 必设 false 让预检真拦)", { derivedDefaultNote: "缺席 ⇒ 继承主模型" }),
263
- r("MODEL_CIRCUIT_BREAKER", "modelPlane", "boolean", "模型腿熔断开关", { staticDefault: "false" }),
263
+ r("MODEL_CIRCUIT_BREAKER", "modelPlane", "boolean", "模型腿熔断开关", { staticDefault: String(RESILIENCE_DEFAULTS.circuitBreaker.value) }),
264
264
  r("MODEL_CODE_ROLES", "modelPlane", "csv", "给点名 role 附编码 persona(role 表内名字才生效)"),
265
- r("MODEL_CONNECT_TIMEOUT_MS", "modelPlane", "number", "连接超时(ms;0=关)", { staticDefault: "30000" }),
265
+ r("MODEL_CONNECT_TIMEOUT_MS", "modelPlane", "number", "连接超时(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.connectTimeoutMs.value) }),
266
266
  r("MODEL_CONTEXT_WINDOW", "modelPlane", "number", "主模型上下文窗声明", { staticDefault: "262144", configKey: "model", resolve: (c) => c.model.contextWindow ?? null, center: cModels("models[].contextWindow") }),
267
- r("MODEL_COST_CACHE_READ", "modelPlane", "number", "主模型 cache-read 单价(per-1M USD;0=不计价)", { staticDefault: "0", danger: BILL, resolve: (c) => c.model.cost?.cacheRead ?? null, center: cModels("models[].cost.cacheRead") }),
268
- r("MODEL_COST_CACHE_WRITE", "modelPlane", "number", "主模型 cache-write 单价", { staticDefault: "0", danger: BILL, resolve: (c) => c.model.cost?.cacheWrite ?? null, center: cModels("models[].cost.cacheWrite") }),
269
- r("MODEL_COST_INPUT", "modelPlane", "number", "主模型 input 单价(NaN 会让成本天花板整条消失 ⇒ 坏值拒启)", { staticDefault: "0", danger: BILL, resolve: (c) => c.model.cost?.input ?? null, center: cModels("models[].cost.input") }),
270
- r("MODEL_COST_OUTPUT", "modelPlane", "number", "主模型 output 单价", { staticDefault: "0", danger: BILL, resolve: (c) => c.model.cost?.output ?? null, center: cModels("models[].cost.output") }),
267
+ r("MODEL_COST_CACHE_READ", "modelPlane", "number", "主模型 cache-read 单价(per-1M USD;显式 0=声明免费,不是「没配」)", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.cacheRead ?? null, center: cModels("models[].cost.cacheRead") }),
268
+ r("MODEL_COST_CACHE_WRITE", "modelPlane", "number", "主模型 cache-write 单价", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.cacheWrite ?? null, center: cModels("models[].cost.cacheWrite") }),
269
+ r("MODEL_COST_INPUT", "modelPlane", "number", "主模型 input 单价(NaN 会让成本天花板整条消失 ⇒ 坏值拒启)", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.input ?? null, center: cModels("models[].cost.input") }),
270
+ r("MODEL_COST_OUTPUT", "modelPlane", "number", "主模型 output 单价", { derivedDefaultNote: "四键全缺席 ⇒ 未定价(不铸 cost 键,成本读面缺席);任一设 ⇒ 四键在场,未设的按 0 显式", danger: BILL, resolve: (c) => c.model.cost?.output ?? null, center: cModels("models[].cost.output") }),
271
271
  r("MODEL_DEFAULT_THINKING", "modelPlane", "enum", "主模型默认思考档(llm-core ThinkingLevel;off=不设模型级默认;未知词拒启)", { enumValues: ["minimal", "low", "medium", "high", "xhigh", "max", "off"], derivedDefaultNote: "缺席/off ⇒ 不铸 defaultThinking(逐任务档由 spec>role 决定)", resolve: (c) => c.model.defaultThinking ?? null }),
272
272
  r("MODEL_DEGRADE_AT_COST_FRACTION", "modelPlane", "number", "预算降级触发比(spent/budget;>1=实际别降级)", { staticDefault: "0.7", danger: BILL }),
273
273
  r("MODEL_DEGRADE_ON", "modelPlane", "csv", "反应式降级触发词表(core DegradeReason 全集;拼错拒启)", { center: { domain: "models", path: "degrade", centerKey: "models", precedence: "center-wins", timing: "restart", restartSlice: "degrade-route" } }),
@@ -275,12 +275,12 @@ export const CONFIG_CATALOG = [
275
275
  r("MODEL_DEGRADE_TO", "modelPlane", "string", "降级目标模型(设了=开降级;外部网关名不必在目录)", { danger: BILL }),
276
276
  r("MODEL_DEGRADE_TO_VISION", "modelPlane", "boolean", "降级目标的 vision 声明(false ⇒ 带图任务丢降级不丢图)", { staticDefault: "false" }),
277
277
  r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
278
- r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT) }),
278
+ r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
279
279
  r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
280
280
  r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
281
281
  r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV)", { configKey: "gatewayFallbackUrls" }),
282
282
  r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
283
- r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: "300000" }),
283
+ r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
284
284
  r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
285
285
  r("MODEL_PRESENCE_PENALTY", "modelPlane", "number", "presence_penalty 直通"),
286
286
  r("MODEL_PROVIDER", "modelPlane", "string", "主模型 provider(路由选 brain)", { derivedDefaultNote: "缺席:ANTHROPIC base+cred 同在 ⇒ anthropic,否则 gateway", resolve: (c) => c.model.provider, center: cModels("models[].provider") }),
@@ -340,7 +340,7 @@ export const CONFIG_CATALOG = [
340
340
  r("QUESTION_TTL_MS", "approval", "number", "AskUserQuestion 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ 出厂窗 300000" }),
341
341
  r("RATE_LIMIT_RPM", "limitsHttp", "number", "计费提交每分钟限流(0=关)", { staticDefault: "0", danger: BILL, configKey: "rateLimitPerMin", center: cLimits("rateLimitPerMin") }),
342
342
  r("READ_DENY_BUILTIN_EXCLUDE", "orchestration", "csv", "按行名删内建 deny 条目(档位之后生效;绝不削任务显式 deny)", { danger: SEC, configKey: "readDenyBuiltinExclude", center: cRead("denyBuiltinExclude", "readDenyBuiltinExclude") }),
343
- r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集四档;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
343
+ r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集五档 credentials/shell-history/browser/wallet/agent-config;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
344
344
  r("READ_DENY_PATTERNS", "orchestration", "csv", "READ 面额外 deny pattern 表", { danger: SEC, configKey: "readDenyPatterns", center: cRead("denyPatterns", "readDenyPatterns") }),
345
345
  r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
346
346
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
@@ -1,6 +1,6 @@
1
1
  import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/settings-schema";
2
2
  import { sealedKeyPoison } from "../sealed-key.js";
3
- import { applyAutoCompactWindow, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
3
+ import { applyAutoCompactWindow, buildModelCostKey, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
4
4
  import { buildModelCompat, readModelCompat } from "../model-compat.js";
5
5
  import { validateCommandRules } from "../runtime-governance.js";
6
6
  import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabilities/collab-workflows.js";
@@ -37,9 +37,7 @@ function toModel(m, envDefaults = {}, logger) {
37
37
  ...(charsPerTokenOf(m.charsPerToken) !== undefined || envDefaults.charsPerToken !== undefined
38
38
  ? { charsPerToken: charsPerTokenOf(m.charsPerToken) ?? envDefaults.charsPerToken }
39
39
  : {}),
40
- cost: m.cost
41
- ? { input: m.cost.input, output: m.cost.output, cacheRead: m.cost.cacheRead ?? 0, cacheWrite: m.cost.cacheWrite ?? 0 }
42
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
40
+ ...buildModelCostKey(m.cost ? { input: m.cost.input, output: m.cost.output, cacheRead: m.cost.cacheRead ?? 0, cacheWrite: m.cost.cacheWrite ?? 0 } : undefined),
43
41
  params: { ...(m.tier ? { tier: m.tier } : {}), promptCacheFamily: m.provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
44
42
  ...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
45
43
  ...(compat ? { compat } : {}),
@@ -45,5 +45,5 @@ export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, app
45
45
  export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
46
46
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
47
47
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
48
- export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
48
+ export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterModel, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
49
49
  //# sourceMappingURL=facade.d.ts.map
@@ -19,8 +19,8 @@ function enabledOnly(rows) {
19
19
  return rows.filter((r) => r.enabled !== false);
20
20
  }
21
21
  function catalogEntryFingerprint(entry) {
22
- const { cost: _cost, quotaWeight: _quotaWeight, enabled: _enabled, ...route } = entry;
23
- return route;
22
+ const { cost, quotaWeight: _quotaWeight, enabled: _enabled, ...route } = entry;
23
+ return { ...route, costDeclared: cost !== undefined };
24
24
  }
25
25
  function planeEntriesFingerprint(rows) {
26
26
  const enabled = enabledOnly(rows);
@@ -4,6 +4,7 @@ import { skillContentHash } from "@sema-agent/settings-schema";
4
4
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
5
5
  import { fetchSkillContent } from "./http-client.js";
6
6
  import { SHA256_HEX_RE } from "../digest-form.js";
7
+ import { describeBadWireName, isUsableWireName } from "../wire-name-bound.js";
7
8
  export async function applyCenterSkills(baseline, manifest, baseUrl, token, logger, fetchImpl = fetch, diskCacheDir) {
8
9
  const { promises: fsp } = await import("node:fs");
9
10
  const { join: joinPath } = await import("node:path");
@@ -84,6 +85,13 @@ export function resolveMcpServers(mcp, logger) {
84
85
  for (const s of mcp.servers ?? []) {
85
86
  if (s.enabled === false)
86
87
  continue;
88
+ if (!isUsableWireName(s.name)) {
89
+ logger?.warn("sema_registry_mcp_name_unusable", {
90
+ server: describeBadWireName(s.name),
91
+ note: "server SKIPPED — an MCP server name is its identity (it becomes the tool namespace prefix) and core REFUSES an out-of-bound name at prepare, which would fail every task in this scenario. Shorten the name and republish; sibling servers are unaffected.",
92
+ });
93
+ continue;
94
+ }
87
95
  const allow = s.allowTools && s.allowTools.length > 0 ? { allowTools: s.allowTools } : {};
88
96
  const elicit = s.elicitation === true ? { elicitation: true } : {};
89
97
  let contentOrigin = {};
@@ -144,6 +152,13 @@ export function resolveA2aPeers(a2a, logger) {
144
152
  for (const p of a2a.peers ?? []) {
145
153
  if (p.enabled === false)
146
154
  continue;
155
+ if (!isUsableWireName(p.name)) {
156
+ logger?.warn("sema_registry_a2a_name_unusable", {
157
+ peer: describeBadWireName(p.name),
158
+ note: "peer SKIPPED — an A2A peer name is its identity (it lands as the roster's `origin.peer`) and core REFUSES an out-of-bound name at prepare, which would fail every task in this scenario. Shorten the name and republish; sibling peers are unaffected.",
159
+ });
160
+ continue;
161
+ }
147
162
  const headers = resolveRefs(p.headerRefs, p.name, "header", logger, "sema_registry_a2a_env_missing", "skipping this A2A peer — referenced env var is unset in this service");
148
163
  if (headers === null)
149
164
  continue;
@@ -56,23 +56,67 @@ export interface RetentionSweepConfig {
56
56
  */
57
57
  export declare const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
58
58
  /**
59
- * S-202 —— **首 token 看门狗的缺省上限(ms)**,全仓单一属主。
59
+ * S-220 —— **`resilience` 块缺省的唯一属主**(S-202 的单源从一根扩到整族)。
60
60
  *
61
- * 「SSE 已开但迟迟不吐第一个 delta」是唯一一条 core 的 idle 计时器够不着的挂死路(它只在首 token
62
- * 之后才武装),所以这条看门狗是那条路上唯一的守卫。600s 而不是原来的 120s:自托管后端在长 prefill
63
- * (大上下文 / 冷缓存 / 排队)下,首字合理地就是要等几分钟——120s 会把一次**正常**的慢启动判成
64
- * `[network]` 失败去重试,而重试再交同一份 prompt 给同一个排队中的后端,只会更慢更贵。
61
+ * 这是一张**表**,不是六个常量:缺省长在三条腿上(解析器腿 `config.ts` 的 `numEnv`/`boolEnv` 缺省、
62
+ * 目录腿 `config-catalog.ts` 的 `staticDefault`、`brain.ts` 的 `DEFAULT_RESILIENCE` —— config 对象
63
+ * 没带 resilience 块时的同姿态兜底),而三条腿此前**各写一份字面量**。S-202 只把 `firstTokenTimeoutMs`
64
+ * 一根收成单源,于是剩下五根还在「改一处漏一处 = 目录对外宣告的缺省与真跑的不是同一代」这条病里 ——
65
+ * 按类修的形是把**整族**收进一张表,让三条腿都只许引用它,而不是再加五个同形常量(六个常量 = 六份
66
+ * 要各自记得引用的纪律;一张表 = 一条)。
65
67
  *
66
- * ⚠️ **`0 = 关`语义不变**,提高缺省不碰它;依赖 120s 快失败的部署把
67
- * `MODEL_FIRST_TOKEN_TIMEOUT_MS=120000` 显式写回即可(env 恒赢)。
68
+ * 每行带自己的 **env 键名** —— 它是这一行的**身份**,给消费门当锚(目录行查名、三腿对账)。
69
+ * ⚠️ 但 `config.ts` / `config-catalog.ts` 的读点仍写**字面** env 名,不写 `RESILIENCE_DEFAULTS.x.env`:
70
+ * D1 双向对账门(`test/config-catalog.test.ts` ⑦)是 AST 扫 `numEnv("NAME", …)` 的字面名来数「src/ 在读
71
+ * 哪些键」,改成间接引用会让这六个键在那道门眼里整体消失(实测:6 个幽灵键 + 动态读棘轮 27→33)——
72
+ * 拿一条真在岗的对账门去换一处写法上的统一是亏的。键名的同源由那道门自己咬:src/ 字面名 ⟺ 目录行名
73
+ * ⟺ 本表的 `env`(S-220 那一格按 `env` 查目录行,查不到当场红),三方互钉,没有静默漂移的空间。
74
+ * `satisfies Readonly<Record<keyof ServiceConfigFlat["resilience"], …>>` 双向咬:块上加字段而表里没行
75
+ * ⇒ tsc 红(加字段的人被迫当场回答「它的出厂缺省是多少、归哪个 env」);表里多一行块上没有的键同样红。
68
76
  *
69
- * 🔴 **常量在此、别处只许引用**:解析器腿(`config.ts` 的 `numEnv` 缺省)、目录腿
70
- * (`config-catalog.ts` 的 `staticDefault`)与 `brain.ts` 的 `DEFAULT_RESILIENCE`(config 对象没带
71
- * resilience 块时的同姿态兜底)此前各写一份 `120000` 字面量,改一处漏一处 = 对外宣告的缺省与真跑的
72
- * 不是同一代。`test/config-catalog.test.ts` 的 S-202 两格咬:一格钉三腿同值,一格扫全 `src/` 判
73
- * 「字面量」这一类写法为红(下一份镜像出生即红)。
77
+ * ⚠️ 语义面**一个数字都没动**:`0 = 关`(三个看门狗)与「熔断默认关」照旧,env 恒赢。
78
+ * 首 token 那一行的 600_000 由来(S-202 clay 裁 A):自托管后端在长 prefill(大上下文 / 冷缓存 /
79
+ * 排队)下首字合理地要等几分钟,120s 会把一次**正常**的慢启动判成 `[network]` 失败去重试,而重试
80
+ * 再交同一份 prompt 给同一个排队中的后端只会更慢更贵;依赖旧姿态的部署显式写回
81
+ * `MODEL_FIRST_TOKEN_TIMEOUT_MS=120000` 即可。
82
+ *
83
+ * 🔴 **常量在此、别处只许引用**:`test/config-catalog.test.ts` 的 S-220 两格咬 —— 一格按表逐行钉
84
+ * 三腿同值(目录 staticDefault / 解析器实算值 / {@link buildDefaultResilience}),一格扫全 `src/`
85
+ * 判「字面量」这一类写法为红(下一份镜像出生即红)。
86
+ */
87
+ export declare const RESILIENCE_DEFAULTS: {
88
+ readonly connectTimeoutMs: {
89
+ readonly env: "MODEL_CONNECT_TIMEOUT_MS";
90
+ readonly value: 30000;
91
+ };
92
+ readonly firstTokenTimeoutMs: {
93
+ readonly env: "MODEL_FIRST_TOKEN_TIMEOUT_MS";
94
+ readonly value: 600000;
95
+ };
96
+ readonly idleTimeoutMs: {
97
+ readonly env: "MODEL_IDLE_TIMEOUT_MS";
98
+ readonly value: 300000;
99
+ };
100
+ readonly circuitBreaker: {
101
+ readonly env: "MODEL_CIRCUIT_BREAKER";
102
+ readonly value: false;
103
+ };
104
+ readonly failureThreshold: {
105
+ readonly env: "MODEL_CB_FAILURE_THRESHOLD";
106
+ readonly value: 5;
107
+ };
108
+ readonly cooldownMs: {
109
+ readonly env: "MODEL_CB_COOLDOWN_MS";
110
+ readonly value: 30000;
111
+ };
112
+ };
113
+ /**
114
+ * S-220 —— 「config 对象没带 resilience 块」时的整块兜底,**逐字段派生自 {@link RESILIENCE_DEFAULTS}**。
115
+ *
116
+ * 返回类型把键集钉死(缺键 / 多键都是 tsc 红),所以这里剩下的唯一手写物是键名本身,值一个字面量都没有。
117
+ * 每次调用铸一只新对象(共享一只导出常量会被消费方就地改掉,那是另一条病)。
74
118
  */
75
- export declare const MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT = 600000;
119
+ export declare function buildDefaultResilience(): ServiceConfigFlat["resilience"];
76
120
  /**
77
121
  * [ref] D1/D2 —— consolidation driver 阀门的三根旋钮(语义与三问见
78
122
  * {@link ServiceConfigFlat.memoryConsolidationDriver})。纯数据。
@@ -1375,12 +1419,15 @@ export interface ServiceConfigFlat {
1375
1419
  * settings ⇒ 整块跳过;bypassPermissions 臂不回 shellGate;非 host lane wiring 不建 ⇒ 沙箱 lane 全失效)。
1376
1420
  * rank 合成:与 autonomy 派生值取大(`ask`→`"always"` 压过 `"classify"`),对 base 只升不降。
1377
1421
  * 客户端 settings 折叠在其后、tighten-only,拿不掉这个值;SUP 路由姿态只会把它抬到 `"always"`。
1378
- * 🔴 **语义随 core 版本演进(2026-07-31 核)**:`"classify"` 下「纯读自动放行」这句自 core 2.7.0 起
1379
- * **不再无条件成立** —— [ref] 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
1422
+ * 🔴 **语义随 core 版本演进(2026-07-31 核;core 7.15.0 再核)**:`"classify"` 下「纯读自动放行」这句
1423
+ * 自 core 2.7.0 起**不再无条件成立** —— [ref] 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
1380
1424
  * (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots+additionalReadRoots,
1381
- * cwd=活 handsCwd),
1382
- * **读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,边界是 core 自算的)。
1383
- * 旧 core(<2.7.0)下仍是名字级判定=越界读放行,两者都能工作、后者更严 ⇒ floor 不因此抬。
1425
+ * cwd=活 handsCwd),**读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,
1426
+ * 边界是 core 自算的)。
1427
+ * 🔴 **而自 core 7.15.0([ref])起,那道读边界不再是本旋钮买来的** —— 它**每一档都判**(`off` 含在内),
1428
+ * 本旋钮买的只剩**剩余**风险那一半:`"classify"` = 可证只读且在界内的命令自动放行、其余发问;
1429
+ * `"always"` = 每条命令都发问。⇒ 别再把「越界读会问」写成开这根旋钮的理由(它在不开的部署上同样成立);
1430
+ * 这根旋钮仍然是 tighten-only、仍然无条件施加,语义一字未改。
1384
1431
  * Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a
1385
1432
  * default — a deployment must explicitly choose it); `"always"` = CC-strict parity (every Bash asks). Any
1386
1433
  * other value (incl. `"off"`) is treated as unset — this knob has no "off" value to opt BACK OUT with once a
@@ -1408,8 +1455,13 @@ export interface ServiceConfigFlat {
1408
1455
  * ToolSearch-loads the schema). Assessment says the win is marginal for us (spec.tools total ≈4.2k tokens, of
1409
1456
  * which Agent + the Task quartet ≈3.2k are CC-core posture = must stay inline; the deferrable tail is
1410
1457
  * ≈200-500 tokens/turn) and SendUserFile is deliberately NOT in the defer set (proactive-delivery tool —
1411
- * hiding it behind ToolSearch suppresses exactly the spontaneous use it exists for). MCP tools are ALWAYS
1412
- * auto-deferred by core regardless of this flag. `TOOL_DEFER_LONGTAIL=true` opts in. */
1458
+ * hiding it behind ToolSearch suppresses exactly the spontaneous use it exists for). `TOOL_DEFER_LONGTAIL=true`
1459
+ * opts in.
1460
+ * 🔴 **core 7.15.0([ref])起 `defer:true` 只是「进候选集」的声明,不是「一定占位」的保证**:候选集
1461
+ * (caller spec 的 `defer:true` + 远端协议铸名 + 内置自声明 defer)整体受**一道**阈值门,全或无 ——
1462
+ * 内联字节 ≥ 上下文窗的 **10%** 才延迟,否则全内联且不挂 `ToolSearch`。同一条收窄也让「MCP 工具恒被
1463
+ * core 自动延迟」这句(本行旧文案)作废:缺省窗下一个中小 MCP 名册够不着阈值,首请求带全 schema。
1464
+ * 唯一不受门的是调用方的 `TaskSpec.deferTools` 字面名单。口径全文见 src/capabilities/tool-defer.ts 头注。 */
1413
1465
  toolDeferLongtail: boolean;
1414
1466
  /** LSP code-intelligence tool ([ref] §13.1): mount core's `lsp` tool, routed to an E2B LSP sidecar. Requires
1415
1467
  * REMOTE_EXEC=e2b. Default OFF (the on-the-fly language-server install is heavy until an E2B template bakes it).
@@ -1625,9 +1677,17 @@ export interface ServiceConfigFlat {
1625
1677
  pattern: string;
1626
1678
  caseSensitive: boolean;
1627
1679
  }[];
1628
- /** [ref] 件 G5(core 5.33.0 [ref],clay 裁定件):**内建** deny 表的档位选择(闭集四档:credentials /
1629
- * shell-history / browser / wallet)。`READ_DENY_BUILTIN_TIERS`=逗号分隔;`none`=显式空姿态(core 的
1630
- * `[]`:一档内建都不要);坏档名 / 空串**启动期响亮拒**(校验用 core 自己的 `resolveReadDenyBuiltins`)。
1680
+ /** [ref] 件 G5(core 5.33.0 [ref],clay 裁定件):**内建** deny 表的档位选择(闭集**五档**:credentials /
1681
+ * shell-history / browser / wallet / **agent-config**)。`READ_DENY_BUILTIN_TIERS`=逗号分隔;`none`=显式
1682
+ * 空姿态(core 的 `[]`:一档内建都不要);坏档名 / 空串**启动期响亮拒**(校验用 core 自己的
1683
+ * `resolveReadDenyBuiltins` —— 词表**单一属主在 core**,本仓零镜像:加档这天校验自动认,这段散文是
1684
+ * 唯一要跟手的东西)。
1685
+ * 🔴 **`agent-config`(core 7.14.0 [ref] C-e 新加的第五档)**:agent **自己的配置文件** —— 它们的 `env` /
1686
+ * server 段里明文放着 API key(壳的 `settings.json` env 块、项目 `.mcp.json` 的 server env、引擎宿主
1687
+ * data-root 下的 `.env`)。此前对这类文件的一次结构化读会把密钥**逐字**落进工具结果、从而落进会话记录。
1688
+ * 行名的是**文件**不是配置**目录**(`.sema`/`.claude` 底下还住着 agents/skills/hooks 与记忆库 —— 模型
1689
+ * 当作日常材料读写的东西);同族工具的 token **存储**(`.credentials.json`/`.codex/auth.json` …)归
1690
+ * `credentials` 档,不归本档 ⇒ 为排查 agent 配置而关掉本档的部署,凭证行照样拦着。
1631
1691
  * 缺席=不铸 ⇒ core 自默认(除 shell-history 外全开)。**仅 deployment 席**:TaskSpec 上没有同名键,
1632
1692
  * 任务/受治脚本无法据此放宽自己的部署(core 侧的设计,与本仓「部署级旋钮无条件施加」同向)。 */
1633
1693
  readDenyBuiltinTiers?: readonly string[];
@@ -1,4 +1,22 @@
1
1
  export const RETENTION_MODES = ["audit-only", "enforce"];
2
2
  export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
3
- export const MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT = 600_000;
3
+ export const RESILIENCE_DEFAULTS = {
4
+ connectTimeoutMs: { env: "MODEL_CONNECT_TIMEOUT_MS", value: 30_000 },
5
+ firstTokenTimeoutMs: { env: "MODEL_FIRST_TOKEN_TIMEOUT_MS", value: 600_000 },
6
+ idleTimeoutMs: { env: "MODEL_IDLE_TIMEOUT_MS", value: 300_000 },
7
+ circuitBreaker: { env: "MODEL_CIRCUIT_BREAKER", value: false },
8
+ failureThreshold: { env: "MODEL_CB_FAILURE_THRESHOLD", value: 5 },
9
+ cooldownMs: { env: "MODEL_CB_COOLDOWN_MS", value: 30_000 },
10
+ };
11
+ export function buildDefaultResilience() {
12
+ const d = RESILIENCE_DEFAULTS;
13
+ return {
14
+ connectTimeoutMs: d.connectTimeoutMs.value,
15
+ firstTokenTimeoutMs: d.firstTokenTimeoutMs.value,
16
+ idleTimeoutMs: d.idleTimeoutMs.value,
17
+ circuitBreaker: d.circuitBreaker.value,
18
+ failureThreshold: d.failureThreshold.value,
19
+ cooldownMs: d.cooldownMs.value,
20
+ };
21
+ }
4
22
  //# sourceMappingURL=config-types.js.map
package/dist/config.d.ts CHANGED
@@ -99,6 +99,64 @@ export declare function logConfigDiagnostics(logger: {
99
99
  info?: (event: string, fields?: Record<string, unknown>) => void;
100
100
  debug?: (event: string, fields?: Record<string, unknown>) => void;
101
101
  }): void;
102
+ /** B-089:`Model.cost` 的四项(per-1M USD)。类型从 core 的 `Model` 取,不在本仓重列一份。 */
103
+ type ModelCost = NonNullable<Model["cost"]>;
104
+ /**
105
+ * 🔴 B-089 —— `Model.cost` 这**一个键**的唯一铸点(可展开片段):有价目表 ⇒ 键在;没配 ⇒ **键不出**。
106
+ * 三个铸点(env 主槽 / env cheap 槽 / config-center 车道)共用它,所以「缺席怎么表达」在本仓只有一处答案。
107
+ *
108
+ * `Model.cost` 自 core 7.16.0([ref])起在**类型面**上也是可选的(`dist/engine/llm/types.d.ts` 的
109
+ * `cost?:`),与它一直以来的运行期读法([ref] 的 `isModelPriced`:这个键在不在)对齐 ——
110
+ * 于是本函数不再需要任何类型让步,返回 `{}` 就是「不写这个键」的字面表达。
111
+ *
112
+ * ⚠️ 连带纪律仍在:**读** `Model.cost` 一律先判在场(catalog 行的 `c.model.cost?.input`),
113
+ * 别写 `model.cost.input`;而「这只模型定价了吗」这一问不许在本仓重写一遍,调 `isModelPriced`。
114
+ */
115
+ export declare function buildModelCostKey(cost: ModelCost | undefined): Pick<Model, "cost">;
116
+ /**
117
+ * 🔴 B-089 —— 「配了 $ 成本天花板,但目录里一只带价模型都没有」的判据 + 文案(纯函数,零副作用)。
118
+ *
119
+ * 两道天花板(`MAX_TASK_COST_USD` / `MAX_PRINCIPAL_COST_USD`)量的是 `costUsd`,而一只**未定价**的模型
120
+ * 算不出成本([ref] 之后它连 `costMicroUsd` 这个键都不发)—— 于是这两根旋钮在这台机器上**永不可能
121
+ * 触发**:运营方以为配了 $ 护栏,实际一道都没有,且没有任何一处会响。不拒启([ref] 只要求 loud ——
122
+ * 这是一台**本来就在服务**的机器的既有形,而且「先配上限、稍后配单价」是正当的上线顺序)。
123
+ *
124
+ * ⚠️ 位置即判据(codex r1 [medium] 真复现):**必须拿生效后的目录与限额来判,不能在 `loadConfig` 里判**。
125
+ * `loadConfig` 只见 env 车道,而 `main.ts` 在第一次 effective 拉取**之前**就把 boot 诊断排空了 ——
126
+ * 在那里判会同时犯两个方向的错:center 名册已配价、env 未配 ⇒ **假警**;env 已配价、center 名册撤了价
127
+ * (或天花板只由 center 的 limits 下发)⇒ **漏警**。故本函数只给判据,席位在两只入口各自的
128
+ * **采纳之后**(`main.ts` 的 `createConfigCenterRuntime` 之后 / `run-local.ts` 的 `applyEffective` 之后)。
129
+ *
130
+ * 判据 = **目录里没有任何一只带价模型**,问法与 `budget.buildPricing` 的填表判据是**同一只函数**
131
+ * (S-231:core 7.16.0 导出的 `isModelPriced`;第二参传 `undefined` = 这一问发生在注入表建起来之前,
132
+ * 谓词落在「模型自报价了吗」那一半)。本模块刻意不 import `budget.ts`(那边挂着 metrics/fleet 一串
133
+ * 运行期依赖,config 是纯解析层)—— 单源因此不是「谁 import 谁」,而是两边都问引擎那一只。
134
+ */
135
+ /** 「$ 天花板 × 价目表」的三位事实 —— 告警判据与清警原因的**同一只**读数(S-232 合并复审:清警要说得出为什么清)。 */
136
+ export interface CostCeilingPricingPosture {
137
+ /** 配了的天花板旋钮名(空 = 一根都没配)。 */
138
+ ceilings: string[];
139
+ /** 生效目录里的模型 id(空 = 目录还空着)。 */
140
+ modelIds: string[];
141
+ /** 目录里至少一只模型自报了价(`isModelPriced(m, undefined)`,S-231 单源)。 */
142
+ anyPriced: boolean;
143
+ }
144
+ export declare function costCeilingPricingPosture(models: Record<string, Model>, limits: {
145
+ maxTaskCostUsd: number;
146
+ maxPrincipalCostUsd: number;
147
+ }): CostCeilingPricingPosture;
148
+ /** 「天花板告警此刻**不**成立」的原因(闭集三词);`undefined` = 告警成立。序 = 先判护栏在不在(`no_ceiling`),
149
+ * 再判有没有东西可计价(`empty_catalog`),最后才是「真配回了价」(`priced`)—— 只有最后一词可以说
150
+ * 「护栏能再触发」,前两词下这句话是假的。 */
151
+ export type UnpricedCeilingClearReason = "no_ceiling" | "empty_catalog" | "priced";
152
+ export declare function unpricedCeilingClearReasonOf(p: CostCeilingPricingPosture): UnpricedCeilingClearReason | undefined;
153
+ export declare function buildUnpricedCeilingNotice(models: Record<string, Model>, limits: {
154
+ maxTaskCostUsd: number;
155
+ maxPrincipalCostUsd: number;
156
+ }): {
157
+ event: string;
158
+ fields: Record<string, string>;
159
+ } | undefined;
102
160
  /** [ref] N8/N9 — the BOOLEAN member of this module's env-parser family (`env`/`env2`/`numEnv`/
103
161
  * `numEnvBounded`/`optFinitePositiveEnv`/`enumEnv`/`headerNameEnv`/`firstSetEnv`). Until now booleans were the
104
162
  * one type parsed by a hand-written literal comparison at every single site, and that is exactly why the same
package/dist/config.js CHANGED
@@ -2,14 +2,14 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
5
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isModelPriced, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
6
6
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
9
9
  import { isV2ScopeKey } from "./memory-scope.js";
10
10
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
11
11
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
12
- import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT } from "./config-types.js";
12
+ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS } from "./config-types.js";
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
15
  import { readFaceForPosture } from "./read-face-posture.js";
@@ -260,16 +260,70 @@ function numEnvOr(name, fallback) {
260
260
  return fallback;
261
261
  return parseNumOrFail(name, raw);
262
262
  }
263
- function cheapCostEnv(name, mainPrice) {
263
+ function costEnv(name, emptyString) {
264
264
  const raw = process.env[name];
265
265
  if (raw === undefined)
266
- return mainPrice;
266
+ return undefined;
267
267
  if (raw === "") {
268
+ if (emptyString === "unset")
269
+ return undefined;
268
270
  CONFIG_WARNINGS.push({ env: name, raw: "", used: "0", kind: "empty-preserved" });
269
271
  return 0;
270
272
  }
271
273
  return parseNumOrFail(name, raw);
272
274
  }
275
+ function buildCostOrAbsent(read, inherited) {
276
+ const declared = read.input !== undefined || read.output !== undefined || read.cacheRead !== undefined || read.cacheWrite !== undefined;
277
+ if (!declared && inherited === undefined)
278
+ return undefined;
279
+ return {
280
+ input: read.input ?? inherited?.input ?? 0,
281
+ output: read.output ?? inherited?.output ?? 0,
282
+ cacheRead: read.cacheRead ?? inherited?.cacheRead ?? 0,
283
+ cacheWrite: read.cacheWrite ?? inherited?.cacheWrite ?? 0,
284
+ };
285
+ }
286
+ export function buildModelCostKey(cost) {
287
+ return cost === undefined ? {} : { cost };
288
+ }
289
+ export function costCeilingPricingPosture(models, limits) {
290
+ const entries = Object.values(models);
291
+ return {
292
+ ceilings: [
293
+ ...(limits.maxTaskCostUsd > 0 ? ["MAX_TASK_COST_USD"] : []),
294
+ ...(limits.maxPrincipalCostUsd > 0 ? ["MAX_PRINCIPAL_COST_USD"] : []),
295
+ ],
296
+ modelIds: [...new Set(entries.map((m) => m.id))],
297
+ anyPriced: entries.some((m) => isModelPriced(m, undefined)),
298
+ };
299
+ }
300
+ export function unpricedCeilingClearReasonOf(p) {
301
+ if (p.ceilings.length === 0)
302
+ return "no_ceiling";
303
+ if (p.modelIds.length === 0)
304
+ return "empty_catalog";
305
+ if (p.anyPriced)
306
+ return "priced";
307
+ return undefined;
308
+ }
309
+ export function buildUnpricedCeilingNotice(models, limits) {
310
+ const posture = costCeilingPricingPosture(models, limits);
311
+ if (unpricedCeilingClearReasonOf(posture) !== undefined)
312
+ return undefined;
313
+ const { ceilings, modelIds } = posture;
314
+ return {
315
+ event: "cost_ceiling_without_pricing",
316
+ fields: {
317
+ ceilings: ceilings.join(","),
318
+ models: modelIds.join(","),
319
+ note: `${ceilings.join(" / ")} is configured but NO model in the effective catalog declares a price table — an ` +
320
+ "unpriced model reports NO cost at all (core RB-368), so these $ ceilings can never fire and this deployment " +
321
+ "has no cost guardrail despite looking like it does. Set MODEL_COST_INPUT / MODEL_COST_OUTPUT / " +
322
+ "MODEL_COST_CACHE_READ / MODEL_COST_CACHE_WRITE (env lane), or a per-model `cost` on the center roster row. " +
323
+ "The token ceiling (MAX_TASK_TOKENS) is unaffected and still fires.",
324
+ },
325
+ };
326
+ }
273
327
  function optNumEnvStrict(name) {
274
328
  const raw = process.env[name];
275
329
  if (!raw)
@@ -753,12 +807,12 @@ function parseModelDomain() {
753
807
  baseUrl: "",
754
808
  reasoning: boolEnv("MODEL_REASONING", true),
755
809
  input: boolEnv("MODEL_VISION", true) ? ["text", "image"] : ["text"],
756
- cost: {
757
- input: numEnv("MODEL_COST_INPUT", "0"),
758
- output: numEnv("MODEL_COST_OUTPUT", "0"),
759
- cacheRead: numEnv("MODEL_COST_CACHE_READ", "0"),
760
- cacheWrite: numEnv("MODEL_COST_CACHE_WRITE", "0"),
761
- },
810
+ ...buildModelCostKey(buildCostOrAbsent({
811
+ input: costEnv("MODEL_COST_INPUT", "unset"),
812
+ output: costEnv("MODEL_COST_OUTPUT", "unset"),
813
+ cacheRead: costEnv("MODEL_COST_CACHE_READ", "unset"),
814
+ cacheWrite: costEnv("MODEL_COST_CACHE_WRITE", "unset"),
815
+ }, undefined)),
762
816
  contextWindow: numEnv("MODEL_CONTEXT_WINDOW", "262144"),
763
817
  maxTokens: process.env.MODEL_MAX_TOKENS !== undefined && process.env.MODEL_MAX_TOKENS !== ""
764
818
  ? parseNumOrFail("MODEL_MAX_TOKENS", process.env.MODEL_MAX_TOKENS)
@@ -776,12 +830,12 @@ function parseModelDomain() {
776
830
  id: cheapId,
777
831
  name: cheapId,
778
832
  input: ((v) => (v === undefined ? model.input : v ? ["text", "image"] : ["text"]))(parseBoolWord("MODEL_CHEAP_VISION", process.env.MODEL_CHEAP_VISION)),
779
- cost: {
780
- input: cheapCostEnv("MODEL_CHEAP_COST_INPUT", model.cost.input),
781
- output: cheapCostEnv("MODEL_CHEAP_COST_OUTPUT", model.cost.output),
782
- cacheRead: cheapCostEnv("MODEL_CHEAP_COST_CACHE_READ", model.cost.cacheRead),
783
- cacheWrite: cheapCostEnv("MODEL_CHEAP_COST_CACHE_WRITE", model.cost.cacheWrite),
784
- },
833
+ ...buildModelCostKey(buildCostOrAbsent({
834
+ input: costEnv("MODEL_CHEAP_COST_INPUT", "declares-zero"),
835
+ output: costEnv("MODEL_CHEAP_COST_OUTPUT", "declares-zero"),
836
+ cacheRead: costEnv("MODEL_CHEAP_COST_CACHE_READ", "declares-zero"),
837
+ cacheWrite: costEnv("MODEL_CHEAP_COST_CACHE_WRITE", "declares-zero"),
838
+ }, model.cost)),
785
839
  contextWindow: numEnvOr("MODEL_CHEAP_CONTEXT_WINDOW", model.contextWindow),
786
840
  maxTokens: numEnvOr("MODEL_CHEAP_MAX_TOKENS", model.maxTokens),
787
841
  ...(optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHEAP_CHARS_PER_TOKEN") } : {}),
@@ -855,12 +909,12 @@ function parseModelDomain() {
855
909
  }
856
910
  : undefined,
857
911
  resilience: {
858
- connectTimeoutMs: numEnv("MODEL_CONNECT_TIMEOUT_MS", "30000"),
859
- firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT)),
860
- idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", "300000"),
861
- circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", false),
862
- failureThreshold: numEnv("MODEL_CB_FAILURE_THRESHOLD", "5"),
863
- cooldownMs: numEnv("MODEL_CB_COOLDOWN_MS", "30000"),
912
+ connectTimeoutMs: numEnv("MODEL_CONNECT_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.connectTimeoutMs.value)),
913
+ firstTokenTimeoutMs: numEnv("MODEL_FIRST_TOKEN_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value)),
914
+ idleTimeoutMs: numEnv("MODEL_IDLE_TIMEOUT_MS", String(RESILIENCE_DEFAULTS.idleTimeoutMs.value)),
915
+ circuitBreaker: boolEnv("MODEL_CIRCUIT_BREAKER", RESILIENCE_DEFAULTS.circuitBreaker.value),
916
+ failureThreshold: numEnv("MODEL_CB_FAILURE_THRESHOLD", String(RESILIENCE_DEFAULTS.failureThreshold.value)),
917
+ cooldownMs: numEnv("MODEL_CB_COOLDOWN_MS", String(RESILIENCE_DEFAULTS.cooldownMs.value)),
864
918
  },
865
919
  model,
866
920
  envModel: model,
@@ -14,6 +14,7 @@
14
14
  * test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
15
15
  */
16
16
  import { type Autonomy } from "../runtime-governance.js";
17
+ import type { ShellGateDoctrine } from "@sema-agent/core";
17
18
  import { type RunRecord } from "../plugins/store-contracts.js";
18
19
  import { type PendingRowBinding } from "../plugins/checkpoint-store-sql.js";
19
20
  /** [ref] 归因的部署侧入参(两个治理旋钮;判据属主见 `governanceMandatesShellGateAlways`)。 */
@@ -124,7 +125,7 @@ export declare function resumeEntryForGate(kind: string, ids: {
124
125
  }): string | null;
125
126
  export declare function governanceOriginOf(gate: {
126
127
  riskDescriptor?: {
127
- shellGateDoctrine?: "classify" | "always";
128
+ shellGateDoctrine?: ShellGateDoctrine;
128
129
  };
129
130
  } | undefined, governance: GovernancePosture | undefined): true | undefined;
130
131
  /** token 泛型:真身是 branded CheckpointToken(秘密能力,只在本函数内部流转,绝不进响应体)。 */
@@ -183,7 +184,7 @@ interface ConflictProbeDeps<TToken> {
183
184
  gate?: {
184
185
  kind?: string;
185
186
  riskDescriptor?: {
186
- shellGateDoctrine?: "classify" | "always";
187
+ shellGateDoctrine?: ShellGateDoctrine;
187
188
  };
188
189
  };
189
190
  } | null | undefined>;