@sema-agent/server 7.129.1 → 7.131.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 (117) hide show
  1. package/MIGRATION.md +102 -0
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +48 -9
  5. package/dist/audit.js +4 -1
  6. package/dist/bench/s1/live-deps.js +2 -0
  7. package/dist/boot/resolve-spec.js +43 -16
  8. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  9. package/dist/boot/stage-10-http-server.d.ts +1 -1
  10. package/dist/boot/stage-10-http-server.js +7 -2
  11. package/dist/capabilities/builtin-tools.js +2 -1
  12. package/dist/capabilities/composite-members.d.ts +1 -0
  13. package/dist/capabilities/composite-members.js +1 -0
  14. package/dist/capabilities/hands-lane.js +2 -2
  15. package/dist/capabilities/host-tool-names.d.ts +17 -0
  16. package/dist/capabilities/host-tool-names.js +15 -0
  17. package/dist/capabilities/mcp-probe.js +19 -6
  18. package/dist/capabilities/repo-tools.js +4 -3
  19. package/dist/capabilities/scenarios.d.ts +34 -0
  20. package/dist/capabilities/scenarios.js +20 -4
  21. package/dist/capabilities/select-environment-tool.js +2 -1
  22. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  23. package/dist/capabilities/send-user-file-tool.js +2 -2
  24. package/dist/capabilities/skills.js +1 -1
  25. package/dist/capabilities/tool-defer.js +2 -1
  26. package/dist/config-catalog.js +16 -5
  27. package/dist/config-invariants.js +2 -2
  28. package/dist/config-provider.js +2 -2
  29. package/dist/config-types.d.ts +6 -0
  30. package/dist/config.d.ts +2 -2
  31. package/dist/config.js +18 -17
  32. package/dist/elicitation.d.ts +7 -0
  33. package/dist/elicitation.js +20 -5
  34. package/dist/exec-bounds.d.ts +6 -6
  35. package/dist/hooks/hook-llm.d.ts +11 -1
  36. package/dist/hooks/hook-llm.js +11 -14
  37. package/dist/hooks/hook-runner.d.ts +55 -9
  38. package/dist/hooks/hook-runner.js +86 -16
  39. package/dist/http/admission.js +2 -2
  40. package/dist/http/approval-sweeps.js +2 -2
  41. package/dist/http/checkpoint-ctx.d.ts +12 -11
  42. package/dist/http/checkpoint-ctx.js +4 -4
  43. package/dist/http/dispatch.js +1 -1
  44. package/dist/http/resume-legs.js +6 -5
  45. package/dist/http/routes/approvals-assistant.js +1 -1
  46. package/dist/http/routes/capabilities.js +13 -5
  47. package/dist/http/routes/runs.js +18 -14
  48. package/dist/http/routes/sessions.js +22 -10
  49. package/dist/http/routes/tasks.js +16 -11
  50. package/dist/http/scenario-read-face.d.ts +14 -0
  51. package/dist/http/scenario-read-face.js +5 -0
  52. package/dist/http/send.d.ts +10 -0
  53. package/dist/http/send.js +6 -0
  54. package/dist/http/server.d.ts +16 -4
  55. package/dist/http/server.js +2 -2
  56. package/dist/http/stored-body-replay.d.ts +10 -3
  57. package/dist/http/stored-body-replay.js +27 -6
  58. package/dist/http/wire-types.d.ts +18 -5
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/mcp-assertion-header.d.ts +2 -2
  62. package/dist/mcp-assertion-header.js +3 -3
  63. package/dist/model-entry-refusal.d.ts +1 -7
  64. package/dist/model-route-endpoint.d.ts +51 -11
  65. package/dist/model-route-endpoint.js +45 -23
  66. package/dist/model-route-gap.d.ts +5 -4
  67. package/dist/model-route-gap.js +1 -1
  68. package/dist/model-select.d.ts +58 -0
  69. package/dist/model-select.js +31 -0
  70. package/dist/namespaced-rule-name.d.ts +7 -4
  71. package/dist/namespaced-rule-name.js +4 -3
  72. package/dist/observability/fail-open.d.ts +16 -7
  73. package/dist/observability/fail-open.js +10 -2
  74. package/dist/plugins/local-session-store.js +2 -2
  75. package/dist/plugins/one-time-migrations.d.ts +21 -1
  76. package/dist/plugins/one-time-migrations.js +33 -1
  77. package/dist/plugins/remote-env-adb.js +5 -4
  78. package/dist/plugins/remote-env-local-docker.js +5 -4
  79. package/dist/plugins/remote-env-ssh.js +5 -4
  80. package/dist/question.d.ts +18 -9
  81. package/dist/question.js +21 -13
  82. package/dist/request-key-closure.d.ts +2 -2
  83. package/dist/request-key-closure.js +16 -9
  84. package/dist/rule-entry-list.d.ts +124 -17
  85. package/dist/rule-entry-list.js +140 -24
  86. package/dist/run-local.d.ts +3 -3
  87. package/dist/run-local.js +8 -7
  88. package/dist/runs.d.ts +42 -2
  89. package/dist/runs.js +45 -2
  90. package/dist/runtime-caps-resolver.d.ts +7 -0
  91. package/dist/runtime-caps-resolver.js +1 -0
  92. package/dist/session-titler.d.ts +4 -1
  93. package/dist/session-titler.js +13 -3
  94. package/dist/spec-fields.d.ts +8 -4
  95. package/dist/stored-request-hooks-m20.d.ts +23 -0
  96. package/dist/stored-request-hooks-m20.js +124 -0
  97. package/dist/stored-request-tool-axes-m19.d.ts +29 -0
  98. package/dist/stored-request-tool-axes-m19.js +27 -0
  99. package/dist/task-mcp.d.ts +55 -60
  100. package/dist/task-mcp.js +197 -96
  101. package/dist/task-settings.d.ts +41 -5
  102. package/dist/task-settings.js +39 -25
  103. package/dist/tool-axis-words.d.ts +14 -17
  104. package/dist/trace/core-keyset-guard.d.ts +2 -2
  105. package/dist/trace/ledger-sink.d.ts +29 -0
  106. package/dist/trace/ledger-sink.js +11 -0
  107. package/dist/trace/project.d.ts +6 -0
  108. package/dist/trace/project.js +11 -0
  109. package/dist/trace/wire-projection-faces.d.ts +1 -1
  110. package/dist/trace/wire-projection-faces.js +1 -1
  111. package/dist/user-authored-text.d.ts +7 -0
  112. package/dist/user-authored-text.js +3 -0
  113. package/dist/web-admin-client/apply-effective.js +10 -5
  114. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  115. package/dist/web-admin-client/types.d.ts +1 -1
  116. package/package.json +2 -2
  117. package/scripts/drain-check-local.mjs +3 -2
@@ -1,5 +1,5 @@
1
1
  import { dispatchBrainLane } from "./model-provider.js";
2
- import { DEFAULT_MODEL_REF } from "./model-select.js";
2
+ import { DEFAULT_MODEL_REF, servedModelTableLeg } from "./model-select.js";
3
3
  export const MODEL_ROUTE_ENDPOINT_MISSING_CODE = "config.model_route_endpoint_missing";
4
4
  export const GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE = "config.gateway_fallback_without_primary";
5
5
  export const MODEL_ROUTE_GAP_WORDS = ["endpoint_missing", "lane_credential_missing", "none"];
@@ -10,14 +10,19 @@ function declared(url) {
10
10
  export function openaiRouteBaseUrl(routeBaseUrl, gatewayBaseUrl) {
11
11
  return declared(routeBaseUrl) ?? declared(gatewayBaseUrl);
12
12
  }
13
- export function resolveOpenaiRouteEndpoint(routeName, routeBaseUrl, gatewayBaseUrl) {
14
- const baseUrl = openaiRouteBaseUrl(routeBaseUrl, gatewayBaseUrl);
15
- return baseUrl !== undefined ? { ok: true, baseUrl } : { ok: false, refusal: routeEndpointMissingRefusal([`"${routeName}"`]) };
13
+ function unresolvedRouteLabel(name, provider) {
14
+ return provider === "anthropic"
15
+ ? `"${name}" (provider "anthropic", but no Anthropic route is mounted — ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN are unset — so it falls to the openai-compatible lane)`
16
+ : `"${name}"`;
16
17
  }
17
- export function modelRouteEndpointRefusal(view) {
18
- return judgeModelRouteEndpoint(view)?.reason;
18
+ export function resolveOpenaiRouteEndpoint(routeName, route, gatewayBaseUrl, leg) {
19
+ const baseUrl = openaiRouteBaseUrl(route?.baseUrl, gatewayBaseUrl);
20
+ return baseUrl !== undefined ? { ok: true, baseUrl } : { ok: false, refusal: routeEndpointMissingRefusal([unresolvedRouteLabel(routeName, route?.provider)], leg) };
19
21
  }
20
- export function judgeModelRouteEndpoint(view) {
22
+ export function modelRouteEndpointRefusal(view, leg) {
23
+ return judgeModelRouteEndpoint(view, leg)?.reason;
24
+ }
25
+ export function judgeModelRouteEndpoint(view, leg) {
21
26
  const gateway = declared(view.gatewayBaseUrl);
22
27
  if (gateway === undefined && view.gatewayFallbackUrls.length > 0) {
23
28
  const reason = (`env MODEL_GATEWAY_FALLBACK_URLS is set but MODEL_GATEWAY_BASEURL is not [${GATEWAY_FALLBACK_WITHOUT_PRIMARY_CODE}] — ` +
@@ -38,28 +43,45 @@ export function judgeModelRouteEndpoint(view) {
38
43
  continue;
39
44
  if (openaiRouteBaseUrl(m.baseUrl, gateway) !== undefined)
40
45
  continue;
41
- const anthropicWordWithoutRoute = m.provider === "anthropic";
42
- laneCredentialMissing ||= anthropicWordWithoutRoute;
43
- unresolved.push(anthropicWordWithoutRoute
44
- ? `"${name}" (provider "anthropic", but no Anthropic route is mounted — ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN are unset — so it falls to the openai-compatible lane)`
45
- : `"${name}"`);
46
+ laneCredentialMissing ||= m.provider === "anthropic";
47
+ unresolved.push(unresolvedRouteLabel(name, m.provider));
46
48
  }
47
49
  if (unresolved.length === 0)
48
50
  return undefined;
49
- return { code: MODEL_ROUTE_ENDPOINT_MISSING_CODE, gap: laneCredentialMissing ? "lane_credential_missing" : "endpoint_missing", reason: routeEndpointMissingRefusal(unresolved) };
51
+ return { code: MODEL_ROUTE_ENDPOINT_MISSING_CODE, gap: laneCredentialMissing ? "lane_credential_missing" : "endpoint_missing", reason: routeEndpointMissingRefusal(unresolved, leg) };
50
52
  }
51
- function routeEndpointMissingRefusal(quotedNames) {
53
+ function routeEndpointMissingRefusal(quotedNames, leg) {
52
54
  const one = quotedNames.length === 1;
53
55
  const subject = one ? `model ${quotedNames[0]} rides` : `models ${quotedNames.join(", ")} ride`;
54
- return (`${subject} the openai-compatible lane but ${one ? "resolves" : "resolve"} to no endpoint ` +
55
- `[${MODEL_ROUTE_ENDPOINT_MISSING_CODE}] — ${one ? "it has no" : "none has a"} baseUrl of its own, and MODEL_GATEWAY_BASEURL is unset. ` +
56
- "MODEL_GATEWAY_BASEURL has no factory default: the former silent placeholder (this machine's port 8000) let such a " +
56
+ const head = `${subject} the openai-compatible lane but ${one ? "resolves" : "resolve"} to no endpoint [${MODEL_ROUTE_ENDPOINT_MISSING_CODE}] — `;
57
+ const noDefault = "MODEL_GATEWAY_BASEURL has no factory default: the former silent placeholder (this machine's port 8000) let such a " +
57
58
  "deployment start fine and then fail on the first model call, far from the cause (\"fetch failed … ECONNREFUSED\", or " +
58
- "a container dialling its own loopback when compose passes an empty value). " +
59
- `Set it explicitly — for a gateway on this machine: ${LOCAL_GATEWAY_HINT} — or give ${one ? "that model" : "each named model"} its own baseUrl in the catalog. ` +
60
- "(A model defined by MODEL_ID / MODEL_CHEAP_ID has no baseUrl of its own: it needs the gateway, or MODEL_PROVIDER=anthropic " +
61
- "with ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN. It is judged at boot even when a catalog will replace it later — it is what this " +
62
- "process serves whenever no catalog lands: the config source unreachable, a refused candidate, or zero enabled models — unless " +
63
- "CONFIG_REQUIRE_ROSTER=true on a remote registry declares it a placeholder that is never served.)");
59
+ "a container dialling its own loopback when compose passes an empty value). ";
60
+ switch (leg) {
61
+ case "catalog":
62
+ return (head +
63
+ `${one ? "it has no" : "none has a"} baseUrl of its own, and MODEL_GATEWAY_BASEURL is unset. ` +
64
+ noDefault +
65
+ `Set it explicitly — for a gateway on this machine: ${LOCAL_GATEWAY_HINT} — or give ${one ? "that model" : "each named model"} its own baseUrl in the catalog.`);
66
+ case "env":
67
+ return (head +
68
+ `${one ? "it is" : "they are"} defined by env (MODEL_ID / MODEL_CHEAP_ID), which gives ${one ? "it" : "them"} no baseUrl of its own, and MODEL_GATEWAY_BASEURL is unset. ` +
69
+ noDefault +
70
+ `This process serves the env model table because no catalog is in force (none configured, the config source unreachable, the candidate refused, or no enabled model in it). Pick one way out: ` +
71
+ `(1) set MODEL_GATEWAY_BASEURL explicitly — for a gateway on this machine: ${LOCAL_GATEWAY_HINT}; ` +
72
+ "(2) mount the Anthropic route — MODEL_PROVIDER=anthropic with ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN (plus ANTHROPIC_BASE_URL for a non-default endpoint); " +
73
+ "(3) leave MODEL_ID unset and let the default entry of a catalog (config.d/models.json, or the web-admin) serve — a catalog in force replaces the env model table. " +
74
+ "(With CONFIG_REQUIRE_ROSTER=true on a remote registry the env model is declared a placeholder that is never served, and is not judged here.)");
75
+ }
76
+ }
77
+ export function judgeServedModelRoute(config, opts) {
78
+ const leg = servedModelTableLeg(config);
79
+ const view = leg === "env" && opts.envTableIsPlaceholder ? { ...config, models: {} } : config;
80
+ return judgeModelRouteEndpoint(view, leg);
81
+ }
82
+ export function assertServedModelRoute(config, opts) {
83
+ const verdict = judgeServedModelRoute(config, opts);
84
+ if (verdict !== undefined)
85
+ throw new Error(verdict.reason);
64
86
  }
65
87
  //# sourceMappingURL=model-route-endpoint.js.map
@@ -81,8 +81,8 @@ export interface ModelRouteGapOptions {
81
81
  /**
82
82
  * 判官的读数(闭集三词,穷举 switch 消费):
83
83
  * · `none` —— 无缺口,仅此一键;
84
- * · 缺口两词 —— `code` = 起服拒启的机读码(与 `reason` 里的方括号码同一个词;`lane_credential_missing` 也带 `config.model_route_endpoint_missing`),
85
- * `reason` = 起服拒启句原文(点名条目与出路)。
84
+ * · 缺口两词 —— `code` = 机读码(与 `reason` 里的方括号码同一个词;`lane_credential_missing` 也带 `config.model_route_endpoint_missing`),
85
+ * `reason` = 起服判官对 env 表拒启时的句子原文(env 视图:点名条目与三条 env 出路)。
86
86
  */
87
87
  export type ModelRouteGapReading = {
88
88
  readonly gap: "none";
@@ -94,9 +94,10 @@ export type ModelRouteGapReading = {
94
94
  };
95
95
  }[ModelRouteGapBreach];
96
96
  /**
97
- * **起服前可调的模型路由缺口纯判官**(与起服判官同一实现;包根导出)。
97
+ * **起服前可调的模型路由缺口纯判官**(与起服判官判 env 表时同一实现、同一句拒因;包根导出)。答的是 **env 腿**:没有目录在管时,这份 env 起不起得来
98
+ * (目录在场时起服以目录为准,见文件头注)。
98
99
  *
99
- * @param env 一份 env 快照(只读 {@link MODEL_ROUTE_ENV_KEYS} 那八把键;`process.env` 也行 —— 起服段就是这样调的)。
100
+ * @param env 一份 env 快照(只读 {@link MODEL_ROUTE_ENV_KEYS} 那八把键;`process.env` 也行)。
100
101
  * @returns 三词之一;有缺口时带 `code` 与 `reason`(见 {@link ModelRouteGapReading})。
101
102
  */
102
103
  export declare function judgeModelRouteGap(env: ModelRouteEnv, opts?: ModelRouteGapOptions): ModelRouteGapReading;
@@ -72,7 +72,7 @@ export function judgeModelRouteGap(env, opts = {}) {
72
72
  gatewayFallbackUrls: r.gatewayFallbackUrls,
73
73
  anthropic: r.anthropicRouteMounted ? {} : undefined,
74
74
  models,
75
- });
75
+ }, "env");
76
76
  if (verdict === undefined)
77
77
  return { gap: "none" };
78
78
  return { gap: verdict.gap, code: verdict.code, reason: verdict.reason };
@@ -34,6 +34,41 @@ export declare function readAtMention(raw: unknown): AtMention | undefined;
34
34
  * `resolveModel("default", catalog)` 解它)。**单点**:读面要说「这台部署的主模型是谁」时读同一个词
35
35
  * (`auto-mode-face.ts` 的席解析器入参),两处各写一个 `"default"` 字面串正是漂移的成因。 */
36
36
  export declare const DEFAULT_MODEL_REF = "default";
37
+ /**
38
+ * **进程服务的是哪一张模型表**(闭集两词,消费方用穷举 switch —— 加一词不补臂 ⇒ 编译红)。路由端点判官与缺省模型判官的「视图词」:
39
+ * 拒因句的出路按视图说真话,不靠调用方拼句。
40
+ * · `env` —— env 表(`MODEL_ID` / `MODEL_CHEAP_ID` 铸的 `{default, [id]}`):**没有目录在管** —— 没配目录、配置源不可达且无 LKG、
41
+ * 候选被整拒、或候选没有一条启用条目。env 行永远没有自己的 `baseUrl`;
42
+ * · `catalog` —— 目录表:一份目录的模型面落地之后**整张换掉** env 表(`config.model = config.models.default`),引擎从不读 env `MODEL_ID`。
43
+ */
44
+ export declare const MODEL_TABLE_LEGS: readonly ["env", "catalog"];
45
+ export type ModelTableLeg = (typeof MODEL_TABLE_LEGS)[number];
46
+ /**
47
+ * 这个进程**此刻**服务的是哪一张表。读法只有一个事实:`config.model` 与 `config.envModel` 是不是同一个对象 ——
48
+ * `envModel` 在 `loadConfig` 铸一次、此后永不换,并与 `model` 在 boot 那一刻是同一只引用;`model` 的唯一写口是目录的模型面落地
49
+ * (`web-admin-client/apply-effective.ts` 的 commit 段,`config.model = config.models.default`)。所以「`model` 还是那只 env 模型」⇔
50
+ * 没有任何一份目录落过模型面 ⇔ 服务的是 env 表。不另记一个会与它漂开的「目录采纳了吗」旗子。
51
+ */
52
+ export declare function servedModelTableLeg(config: {
53
+ readonly model: object;
54
+ readonly envModel: object;
55
+ }): ModelTableLeg;
56
+ /**
57
+ * 目录服务、而 env 里还设着 `MODEL_ID` ⇒ 启动时说一行(沿 `config_env_set_but_ineffective` 通告族:值合法、却不参与服务)。
58
+ * 这不是拒:目录一旦被采纳就整张换掉 env 表,`MODEL_ID` 在这一形下本来就不是缺省来源(缺省来自目录)—— 运维该知道自己的那一行
59
+ * 没在服务,否则会以为改它有效。env 表服务(或没设 `MODEL_ID`)⇒ `undefined`。
60
+ *
61
+ * 不读 `process.env`:env 的读数在 `loadConfig` 铸 `envModel` 时已定(`envModel.id` 就是 `MODEL_ID`,空串 = 没设)。
62
+ * 返回 plain data(事件名 + 字段)。**唯一发声点** = `web-admin-client/apply-effective.ts` 的 `applyEffective` commit 之后、服务表由 env 换成目录的那一拍(进程一生至多一次):
63
+ * 启动(含 LKG / 按组回落)、`run-local`、迟到 boot、refresh 都经那一个函数,「恰好一次」是构造出来的,没有去重表。
64
+ */
65
+ export declare function buildEnvModelIdNotServedNotice(config: {
66
+ readonly model: Pick<Model, "id" | "name">;
67
+ readonly envModel: Pick<Model, "id">;
68
+ }): {
69
+ readonly event: string;
70
+ readonly fields: Record<string, unknown>;
71
+ } | undefined;
37
72
  /**
38
73
  * 本仓给一条**没选模型**的 task 铸的主模型 —— {@link DEFAULT_MODEL_REF} 在**展开**目录上(与引擎构造期 `expandTiers(models, tiers)`
39
74
  * 之后再解逐字同源)解到的那只**目录条目**(对象引用)。`boot/resolve-spec.ts` 每条 spec 都盖 `spec.model`,所以引擎眼里的 main
@@ -153,6 +188,29 @@ export type ModelImageInput = (typeof MODEL_IMAGE_INPUTS)[number];
153
188
  export declare function modelImageInput(model: {
154
189
  input?: unknown;
155
190
  } | undefined): ModelImageInput;
191
+ /**
192
+ * 「这个目录条目的 `reasoning` 位声明了什么」的**唯一读法**:`true` / `false` 原样,**缺席 = 未声明 = `undefined`**(不替没声明的模型答「是」也不答「否」)。
193
+ * `GET /v1/models` 的 `reasoning` 列与 `GET /v1/config/catalog` 的 `MODEL_REASONING` 行都投影它。非布尔(手写错形)与条目不在,同归未声明:没有可信的声明。
194
+ */
195
+ export declare function modelReasoningDecl(model: {
196
+ reasoning?: unknown;
197
+ } | undefined): boolean | undefined;
198
+ /** 两个能力位的**声明**(三态:`true` / `false` / 缺席 = 未声明)—— 铸 `Model` 上对应键的输入,见 {@link mintCapabilityKeys}。 */
199
+ export interface ModelCapabilityDecl {
200
+ readonly reasoning?: boolean;
201
+ readonly vision?: boolean;
202
+ }
203
+ /**
204
+ * 能力位声明 ⇒ `Model` 上的两个键,**缺席不铸键**:`reasoning` 原样;`vision` 翻成 `input`(`true` ⇒ `["text","image"]`,`false` ⇒ `["text"]`)。
205
+ * env 腿(`MODEL_REASONING` / `MODEL_VISION`)、cheap 槽(`MODEL_CHEAP_*`)与中心腿(`models[].reasoning` / `vision`)都经它铸 —— 三条腿一个铸法、一个「缺席」的意思。
206
+ * 读回的唯一读法是 {@link modelReasoningDecl} 与 {@link modelImageInput}(带图预检与 `GET /v1/models` 同读后者)。
207
+ *
208
+ * 🔴 **本仓唯一的类型接缝**:引擎 7.51.2 的 `Model` 型面把 `reasoning` / `input` 写成必填,而它自己的读法对缺席有定义 ——
209
+ * `input` 缺席 ⇒ 支持图(`modelSupportsVision`:`model?.input === undefined ? true : …`),`reasoning` 缺席 ⇒ 真值判假(`!!model.reasoning`)。所以交给引擎的目录里缺席的键是安全的,
210
+ * 只是型面还没跟上;返回型因此对 `Pick<Model, …>` 说了一句「可能为空」的谎,把断言收在这一处 —— 调用点的 `Model` 字面量照旧带 `Model` 注解(其余每个键仍被 tsc 逐个核,
211
+ * 读表门按注解收字面量的 core 键)。型面归引擎:引擎把这两位放成可选之日,本接缝(返回型 + 这一处断言)整件删除。
212
+ */
213
+ export declare function mintCapabilityKeys(decl: ModelCapabilityDecl): Pick<Model, "reasoning" | "input">;
156
214
  /**
157
215
  * 带图预检:只有目录条目**明说**不收图({@link modelImageInput} 为 `"no"`)才为假 —— 提交时据此计数并告警(引擎会把这些图换成给模型的
158
216
  * 占位句);没声明的条目为真(不出信号,图原样交给模型)。
@@ -7,6 +7,25 @@ export function readAtMention(raw) {
7
7
  return r.success ? r.data : undefined;
8
8
  }
9
9
  export const DEFAULT_MODEL_REF = "default";
10
+ export const MODEL_TABLE_LEGS = ["env", "catalog"];
11
+ export function servedModelTableLeg(config) {
12
+ return config.model === config.envModel ? "env" : "catalog";
13
+ }
14
+ export function buildEnvModelIdNotServedNotice(config) {
15
+ if (servedModelTableLeg(config) !== "catalog" || config.envModel.id === "")
16
+ return undefined;
17
+ const served = config.model.name || config.model.id;
18
+ return {
19
+ event: "config_env_set_but_ineffective",
20
+ fields: {
21
+ env: "MODEL_ID",
22
+ raw: config.envModel.id,
23
+ problem: `a catalog is in force and replaces the env model table — the catalog default "${served}" serves this process, and MODEL_ID (with MODEL_CHEAP_ID) takes no part in it; remove the env line or point the catalog's default at the model you mean`,
24
+ servedBy: "catalog",
25
+ catalogDefault: served,
26
+ },
27
+ };
28
+ }
10
29
  export function resolveMainModel(plane) {
11
30
  const expanded = expandTiers(plane.models, plane.tiers ?? {}) ?? plane.models;
12
31
  return coreResolveTaskModel({ model: DEFAULT_MODEL_REF }, { ...(expanded !== undefined ? { models: expanded } : {}) }).model;
@@ -101,6 +120,18 @@ export function modelImageInput(model) {
101
120
  return "undeclared";
102
121
  return model.input.includes("image") ? "yes" : "no";
103
122
  }
123
+ export function modelReasoningDecl(model) {
124
+ return typeof model?.reasoning === "boolean" ? model.reasoning : undefined;
125
+ }
126
+ const CAPABILITY_MODEL_KEYS = { reasoning: "reasoning", input: "input" };
127
+ export function mintCapabilityKeys(decl) {
128
+ const keys = {};
129
+ if (decl.reasoning !== undefined)
130
+ keys[CAPABILITY_MODEL_KEYS.reasoning] = decl.reasoning;
131
+ if (decl.vision !== undefined)
132
+ keys[CAPABILITY_MODEL_KEYS.input] = decl.vision ? ["text", "image"] : ["text"];
133
+ return keys;
134
+ }
104
135
  export function modelSupportsImages(model) {
105
136
  return modelImageInput(model) !== "no";
106
137
  }
@@ -9,7 +9,7 @@
9
9
  * **调它的面**(点名,不是「每个面」):运维三张名表(`APPROVAL_REQUIRE` / `DENY` / `NEVER_AUTO`)与中心下发的同名表、请求
10
10
  * `settings.permissions.{ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`)。会话规则写口直接调 core 的 `assertSessionRuleNamesCurrent`
11
11
  * (同一只判官的店形);引擎 allow 层的条目表(请求 `settings.permissions.allow`、运维 `APPROVAL_GRANT`、技能两只清单)交引擎 `compileAllowList`
12
- * (名册 = {@link NO_DECLARED_TOOL_NAMES};判官判非现役的条目读成 `tool_name`)。
12
+ * (名册 = {@link TOOL_NAME_ROSTER};判官判非现役的条目读成 `tool_name`,名册里有的只差大小写读成 `near_miss`)。
13
13
  * **不调它的面**:`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)—— 如实未接,
14
14
  * 在那里写铸不出的服务器段仍被收下、永不命中;把它接上判官 = 另一件事(`unmatchableRuleNameOf` 就是现成的那一只)。
15
15
  * 子代的工具名表 `agents[].allowTools` / `denyTools` 不是本文件的面:core 7.45.0 #1377 / #1385 起名字归引擎在开跑前一只判(三来源同判,`spec-fields.ts`
@@ -28,10 +28,13 @@ import { type RuleToolNameVerdict } from "@sema-agent/core";
28
28
  */
29
29
  export declare function ruleEntryToolName(entry: string): string;
30
30
  /**
31
- * 判官的名册:本仓的配置表 / 请求表 / 会话规则写口都不声明「部署在退役名下挂了自有工具」(本仓没有这样的工具;boot / 请求期也没有哪一跑的名册可看)
32
- * ⇒ 恒为空集。引擎判官、`compileAllowList` 的第三参、会话规则店同读这一只。
31
+ * **名册 —— 本仓唯一的一张**:引擎目录的全部工具名(`catalogToolNames()`:引擎自己出的每一只工具的线上名,别名在内)∪ 本仓自己铸给模型的工具名({@link HOST_TOOL_NAMES}:取自铸它们的那一处的常量,不手抄)。
32
+ * 引擎判官 `judgeRuleToolName` 与 `compileAllowList` 的第三参读它:名册里有的名字是「现役」,条目只差大小写的读成 `near_miss`(`BASH` ⇒ 现名 `Bash`、`SENDUSERFILE` ⇒ `SendUserFile`),
33
+ * 退役名 / 无前缀短名 / 对端段铸不出的名字照旧读成 `tool_name`。引擎在开跑时读的是这一 run 真挂着的名册 —— 受理口读不到它,读得到的就是上面这两份:任何部署都带的引擎工具,加本仓自己挂的工具。
34
+ * 每一处读条目名的地方(请求体的规则表与工具名清单、技能清单、运维 / 中心的审批名单、会话规则写口)传这一张;**读成空 = 放行 / 静默收窄**的那些表因此不再有「空名册读不出孪生」的一读。
35
+ * 界:调用方自带的宿主工具(嵌入方 `createHttpServer` 之外自挂的)不在其中 —— 它们的孪生仍只由引擎在开跑时告知运维。
33
36
  */
34
- export declare const NO_DECLARED_TOOL_NAMES: ReadonlySet<string>;
37
+ export declare const TOOL_NAME_ROSTER: ReadonlySet<string>;
35
38
  /** 一个规则名永远匹配不到工具的判词(闭集)= 引擎判官的非现行判词。 */
36
39
  export type UnmatchableRuleName = Exclude<RuleToolNameVerdict, {
37
40
  kind: "current";
@@ -1,11 +1,12 @@
1
- import { PROTOCOL_TABLE, judgeRuleToolName, parsePermissionRule } from "@sema-agent/core";
1
+ import { PROTOCOL_TABLE, catalogToolNames, judgeRuleToolName, parsePermissionRule } from "@sema-agent/core";
2
+ import { HOST_TOOL_NAMES } from "./capabilities/host-tool-names.js";
2
3
  import { displayListEntry } from "./list-entry-display.js";
3
4
  export function ruleEntryToolName(entry) {
4
5
  return parsePermissionRule(entry).toolName;
5
6
  }
6
- export const NO_DECLARED_TOOL_NAMES = new Set();
7
+ export const TOOL_NAME_ROSTER = new Set([...catalogToolNames(), ...HOST_TOOL_NAMES]);
7
8
  export function unmatchableRuleNameOf(name) {
8
- const verdict = judgeRuleToolName(name, NO_DECLARED_TOOL_NAMES);
9
+ const verdict = judgeRuleToolName(name, TOOL_NAME_ROSTER);
9
10
  return verdict.kind === "current" ? undefined : verdict;
10
11
  }
11
12
  export function describeUnmatchableRuleName(v, name) {
@@ -93,7 +93,7 @@ export declare const FAIL_OPEN_TAGS: {
93
93
  };
94
94
  readonly "server.hitl.frame-undelivered-stream-closed": {
95
95
  readonly cls: "P-DEBT";
96
- readonly note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。";
96
+ readonly note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族,且只在**无账本孪生**(无 durable run 行)的部署上——有孪生的腿直写面已死时帧在账本里、不算丢——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。";
97
97
  };
98
98
  readonly "server.stream.frame-dropped-stream-closed": {
99
99
  readonly cls: "F";
@@ -117,7 +117,11 @@ export declare const FAIL_OPEN_TAGS: {
117
117
  };
118
118
  readonly "server.session-titler.gateway-unreachable": {
119
119
  readonly cls: "F";
120
- readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
120
+ readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时 / 非 2xx 应答;端点回 3xx 重定向被拒的那一族另记 `server.session-titler.redirect-refused`,不进本 tag)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
121
+ };
122
+ readonly "server.session-titler.redirect-refused": {
123
+ readonly cls: "F";
124
+ readonly note: "`session-titler.ts logFailure`:自动会话标题那一次廉价模型调用被拒 —— 配置的模型端点回了 3xx 重定向。本服务发往配置端点的直连请求不跟随重定向(跟随会把请求体里的用户内容与凭据头送到 `Location` 指向的另一个 origin),所以这是**端点配置问题,不是网络问题**:出路 = 把该模型的网关 URL(`MODEL_GATEWAY_BASEURL` / 目录条目自己的 baseUrl / `ANTHROPIC_BASE_URL`)改成最终地址,查网络没有用。与 `server.session-titler.gateway-unreachable` 的分界 = 失败码(llm 面交回的 `redirect_refused`),不看文案。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。放行的最坏后果 = 该部署的会话没有自动标题。逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的);`session_title_total{outcome=llm_failed}` 照常逐次记账。";
121
125
  };
122
126
  readonly "server.prompts.artifact-cache-write-failed": {
123
127
  readonly cls: "F";
@@ -239,6 +243,10 @@ export declare const FAIL_OPEN_TAGS: {
239
243
  readonly cls: "F";
240
244
  readonly note: "S-995(`hooks/hook-runner.ts` 的 `toolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 形只问契约包的 `hookMatcherForm`(literal 形〔名字表 / 空 / `*`〕两档都直接问 `hookMatcherSelects`、不占预算;regex 形 regex 档把同一只契约函数交进 vm 带每次求值各一份的固定预算〔超时 = 灾难回溯 ⇒ 确定的 400 拒,不是未判、不登记〕、literal 档不适用;invalid 形 regex 档 = 确定的 400 拒〔S-1455 收尾:不是未判、不登记;运行期另记 `hook_matcher_invalid`〕、literal 档不适用)。现在只有一种 reason —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)。`invalid_regex` 臂自 S-1455 收尾(7.126.0)起退役:它是确定性的坏配置、不是「判不了」,与超预算同类 ⇒ 受理当场 400。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(超长 matcher)显形。";
241
245
  };
246
+ readonly "server.hooks.resume-reader-refused": {
247
+ readonly cls: "P-DEBT";
248
+ readonly note: "续跑重放(`task-settings.ts` 的 `parseTaskSettings`,经 `boot/resolve-spec.ts` 装配)读存下的请求体里的 `settings.hooks`,而 hooks 配置的唯一读者(契约包分类器,`hooks/hook-runner.ts` 的 `parseHooksConfig`)判不过、也没被一次性迁移 M20 接住(窗已关,或拒因不属 M20 的两形 —— 藏着守卫迹象的未知字段 / JSON `__proto__` 事件键)⇒ 这份 hooks **整只不采纳**(`task_settings_deferred` 记账),任务照常续跑 —— 其中若有真在拦的 `PreToolUse` 钩子,续跑后不再拦。M20 窗开着时那两形不进这一臂(剥键重读、守卫照跑,一行 `hooks_read_as_before_upgrade`)。只在 hooks 会被采纳的单用户车道计数(多租户整只忽略 hooks,无物可丢);fresh 提交不进这一臂(受理面 400)。放行的最坏后果:升级前停驻、体里 hooks 带那两形之一的任务,在窗关之后才续跑的(发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行),或体里 hooks 带别的坏形的,失去这份 hooks 里原本真在拦的钩子。方向已裁:持久数据面走 M20 窗内迁移,窗关后整份不采纳 + 本计数(存量停驻行此时已被收割,计数应趋零)。detail = `leg=`。计数非零 = 有任务带着被读者拒掉的存量 hooks 在续跑。";
249
+ };
242
250
  readonly "server.hooks.notice-sink-threw": {
243
251
  readonly cls: "F";
244
252
  readonly note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。";
@@ -470,11 +478,12 @@ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
470
478
  * 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
471
479
  *
472
480
  * ⚠️ **两条 question 臂在生产上已不可达**([ref] 之后,2026-08-07):本函数唯一的生产调用点是
473
- * `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
474
- * `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
475
- * question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
476
- * 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
477
- * 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
481
+ * `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧的直写口
482
+ * (同文件 `writeQuestionLive`)先过断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;
483
+ * 另一条 question 路(`runs.ts` 的 durable append)根本不经 emitAsk。elicitation 族自账本孪生起也只在**无 durable
484
+ * run 行**的部署上抵达这里(有孪生的腿直写面已死时孪生即送达,不碰死连接)。在产的只剩那一形与 default。
485
+ * 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),新增第四条 emit 腿忘了守卫时,
486
+ * 留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
478
487
  */
479
488
  export declare function failOpenTagForDroppedFrame(frameType: string): FailOpenTag;
480
489
  /** {@link createFailOpenRecorder} 返回的活对象(有行为、有状态 ⇒ `create*` 而非 `build*`)。 */
@@ -74,7 +74,7 @@ export const FAIL_OPEN_TAGS = {
74
74
  },
75
75
  "server.hitl.frame-undelivered-stream-closed": {
76
76
  cls: "P-DEBT",
77
- note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。",
77
+ note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族,且只在**无账本孪生**(无 durable run 行)的部署上——有孪生的腿直写面已死时帧在账本里、不算丢——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。",
78
78
  },
79
79
  "server.stream.frame-dropped-stream-closed": {
80
80
  cls: "F",
@@ -98,7 +98,11 @@ export const FAIL_OPEN_TAGS = {
98
98
  },
99
99
  "server.session-titler.gateway-unreachable": {
100
100
  cls: "F",
101
- note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
101
+ note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时 / 非 2xx 应答;端点回 3xx 重定向被拒的那一族另记 `server.session-titler.redirect-refused`,不进本 tag)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
102
+ },
103
+ "server.session-titler.redirect-refused": {
104
+ cls: "F",
105
+ note: "`session-titler.ts logFailure`:自动会话标题那一次廉价模型调用被拒 —— 配置的模型端点回了 3xx 重定向。本服务发往配置端点的直连请求不跟随重定向(跟随会把请求体里的用户内容与凭据头送到 `Location` 指向的另一个 origin),所以这是**端点配置问题,不是网络问题**:出路 = 把该模型的网关 URL(`MODEL_GATEWAY_BASEURL` / 目录条目自己的 baseUrl / `ANTHROPIC_BASE_URL`)改成最终地址,查网络没有用。与 `server.session-titler.gateway-unreachable` 的分界 = 失败码(llm 面交回的 `redirect_refused`),不看文案。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。放行的最坏后果 = 该部署的会话没有自动标题。逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的);`session_title_total{outcome=llm_failed}` 照常逐次记账。",
102
106
  },
103
107
  "server.prompts.artifact-cache-write-failed": {
104
108
  cls: "F",
@@ -220,6 +224,10 @@ export const FAIL_OPEN_TAGS = {
220
224
  cls: "F",
221
225
  note: "S-995(`hooks/hook-runner.ts` 的 `toolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 形只问契约包的 `hookMatcherForm`(literal 形〔名字表 / 空 / `*`〕两档都直接问 `hookMatcherSelects`、不占预算;regex 形 regex 档把同一只契约函数交进 vm 带每次求值各一份的固定预算〔超时 = 灾难回溯 ⇒ 确定的 400 拒,不是未判、不登记〕、literal 档不适用;invalid 形 regex 档 = 确定的 400 拒〔S-1455 收尾:不是未判、不登记;运行期另记 `hook_matcher_invalid`〕、literal 档不适用)。现在只有一种 reason —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)。`invalid_regex` 臂自 S-1455 收尾(7.126.0)起退役:它是确定性的坏配置、不是「判不了」,与超预算同类 ⇒ 受理当场 400。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(超长 matcher)显形。",
222
226
  },
227
+ "server.hooks.resume-reader-refused": {
228
+ cls: "P-DEBT",
229
+ note: "续跑重放(`task-settings.ts` 的 `parseTaskSettings`,经 `boot/resolve-spec.ts` 装配)读存下的请求体里的 `settings.hooks`,而 hooks 配置的唯一读者(契约包分类器,`hooks/hook-runner.ts` 的 `parseHooksConfig`)判不过、也没被一次性迁移 M20 接住(窗已关,或拒因不属 M20 的两形 —— 藏着守卫迹象的未知字段 / JSON `__proto__` 事件键)⇒ 这份 hooks **整只不采纳**(`task_settings_deferred` 记账),任务照常续跑 —— 其中若有真在拦的 `PreToolUse` 钩子,续跑后不再拦。M20 窗开着时那两形不进这一臂(剥键重读、守卫照跑,一行 `hooks_read_as_before_upgrade`)。只在 hooks 会被采纳的单用户车道计数(多租户整只忽略 hooks,无物可丢);fresh 提交不进这一臂(受理面 400)。放行的最坏后果:升级前停驻、体里 hooks 带那两形之一的任务,在窗关之后才续跑的(发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行),或体里 hooks 带别的坏形的,失去这份 hooks 里原本真在拦的钩子。方向已裁:持久数据面走 M20 窗内迁移,窗关后整份不采纳 + 本计数(存量停驻行此时已被收割,计数应趋零)。detail = `leg=`。计数非零 = 有任务带着被读者拒掉的存量 hooks 在续跑。",
230
+ },
223
231
  "server.hooks.notice-sink-threw": {
224
232
  cls: "F",
225
233
  note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。",
@@ -12,7 +12,7 @@ import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "
12
12
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
13
13
  import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
14
14
  import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
15
- import { buildObjectivePreview } from "../user-authored-text.js";
15
+ import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
16
16
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
17
17
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
18
18
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
@@ -21,7 +21,7 @@ function userText(entry) {
21
21
  if (entry.type !== "message")
22
22
  return null;
23
23
  const msg = entry.message;
24
- if (msg.role !== "user")
24
+ if (msg.role !== "user" || isAttachmentFrame(msg))
25
25
  return null;
26
26
  const c = msg.content;
27
27
  if (typeof c === "string")
@@ -170,6 +170,26 @@
170
170
  * **读侧半场**(同一行、同一窗,不另起登记行):由存卡唯一读者入口 `approval-card.ts` `readStoredCardEnvelope`(起服改写够不着的卡 —— 滚动升级窗里
171
171
  * 7.128.x 副本新写的、local 车道的文件 ask 账)与停驻行入口 `currentParkArm`(**所有**停驻行 —— SQL 与 local 车道引擎 File 店的同一条路径)读时过同一只改写,
172
172
  * **响亮**(`server.approval-card.retired-card-word-read`);读的是浅拷贝,被读的行一个字节不动。local 车道读数恒零(`NO_SQL_MIGRATIONS`)。
173
+ * · **M19 续跑重放腿上的存量 `toolAxes`**(7.130.0)—— 请求体 `mcpServers[].toolAxes` 的判官从本版起是引擎的 `readMcpToolAxes`(整份一坏全拒);
174
+ * 升级前的请求腿读者把非对象(含 `null`)整键忽略、对象逐成员剔坏留好,server 照挂。续跑重放腿重放的是升级前存下的请求体(`checkpoint_ctx`),
175
+ * 停驻在那台 server 某只工具审批上的任务,若这台 server 整条不进,批准即撞 `resume.tool_unavailable`、检查点重开、永远批不过去。与 M10 同形:
176
+ * 不改写存下的字节(体按原样留在 ctx 里),是一只**窗内才生效的行为臂** —— 窗开着时(运行期读本表 M19 行的 `removableAfter`,见 {@link migrationWindowOpen}),
177
+ * **只在续跑重放腿**、**只对判官判不过的那一格**按升级前的读法重读一次(`stored-request-tool-axes-m19.ts`),产物再交同一只判官(不过 ⇒ 整条不进的原臂),
178
+ * 真挂上的这类 server 各一行运维 warn `mcp_tool_axes_read_as_before_upgrade`;窗关之后照今天答(整条不进),删码因此是零行为变化。
179
+ * 窗 = 发布日 + 30 天 = 停驻行的终局兜底窗缺省(`APPROVAL_TERMINAL_BACKSTOP_MS` 缺省 30 天)—— 与 M7 / M12 同一条论证。没有起服读数(它不写库)。
180
+ * 窗按**发布日**算、停驻行的期限按**停驻时刻**算 ⇒ 三类部署会有升级前停驻的行活过窗、窗关后回到「整条不进」:发布后才升级的部署(发布日到它升级那天之间
181
+ * 停驻的行)、把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、滚动升级里仍跑 7.129.x 的副本在窗内新写的行。与 M7 / M18 一样如实登记,不为它们加长窗。
182
+ * **它是上游字段标准的限时例外**:引擎对 `toolAxes` 的标准是「坏键不丢、坏词不默认」(core `mcp-server-spec.d.ts`),窗内这一臂恰恰剔坏留好 ——
183
+ * 依据是持久数据律(存量数据不能「变红」,只许一次性迁移、窗关删码),只对升级前已经按这种读法跑过的存量体,且对运维响亮。新提交与部署腿不受它影响。
184
+ * · **M20 续跑重放腿上的存量 `settings.hooks`**(7.131.0)—— hooks 配置的唯一读者是契约包的 `classifyHooksConfig`(经 `parseHooksConfig`);升级前的读者(12.x zod 面)对两形照收:
185
+ * 条目 / 匹配器上**契约不认识的字段里藏着守卫迹象**(未知字段被剥掉,同份里真在拦的 `PreToolUse` 钩子照拦)与 JSON 写出的 **`__proto__` 事件键**(被跳过)。现读者对这两形整份拒,
186
+ * 续跑重放腿重放的是升级前存下的体 ⇒ 停驻任务续跑后整份 hooks 不采纳、守卫不再拦。与 M19 同形:不改写存下的字节,是一只**窗内才生效的行为臂** —— 窗开着时(运行期读本表 M20 行的
187
+ * `removableAfter`,见 {@link migrationWindowOpen}),**只在续跑重放腿**、**只在会采纳 hooks 的单用户车道**、**只对读者拒因全部属这两形**的存体:把读者点名的那一块(条目 / 匹配器)里按类型确属未知的键
188
+ * 一次全剥(12.x 读者对一个条目本来就是剥掉全部未知字段、保留其余),剩下的文档**重新过同一只读者**,过了就采纳;整次重读恰一行运维 warn `hooks_read_as_before_upgrade`(点名 sessionId 与被剥的坐标)。
189
+ * **不复制旧读者**:`stored-request-hooks-m20.ts` 不判值形、不判整份是否合法(那是读者的答案),只做「读者点名了哪些块、这一块里哪些键按它的类型确属未知、整块剥掉再问一次,剥到读者受理为止」;
190
+ * 「确属未知」按该条目自己的 `type` 取契约字段名表(不用各型字段的并集,表由契约类型面穷举)。拒因里混进别的(真坏的守卫条目、点名了本型契约字段的拒因 …)⇒ 不迁移,整份照拒。
191
+ * 窗关之后 ⇒ 续跑整份不采纳,fail-open 登记 `server.hooks.resume-reader-refused` 照打(可见);fresh 腿不受影响(受理面照 400)。没有起服读数(它不写库)。
192
+ * 窗 = 发布日 + 30 天 = 停驻行的终局兜底窗缺省 —— 与 M19 同一条论证与同三类逃出窗的部署(发布后才升级的、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的、滚动升级里旧副本在窗内新写的行),如实登记。
173
193
  *
174
194
  * ## 幂等
175
195
  * 改写的几条都可重复跑(M4:`detail IS NOT NULL AND … = 'null'`;M5:列在场探针 + `WHERE incarnation = ''`;M6:`WHERE error_code IS NULL`;
@@ -295,7 +315,7 @@ export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
295
315
  /**
296
316
  * 一条登记行的窗**此刻**开着没有(`today` = UTC 日历日 YYYY-MM-DD):未发布形(`removableAfter: null`)⇒ 开;否则今天不晚于 `removableAfter` ⇒ 开
297
317
  * (窗关当天仍开,与到期门「今天严格晚于才红」同一条边界)。
298
- * 只给**窗内才生效的行为臂**用(M10 的认领):那种臂过了窗就停,不等删码车 —— 删码因此是零行为变化,而窗只写在本表这一处。
318
+ * 只给**窗内才生效的行为臂**用(M10 的认领、M19 / M20 的续跑重读):那种臂过了窗就停,不等删码车 —— 删码因此是零行为变化,而窗只写在本表这一处。
299
319
  */
300
320
  export declare function migrationWindowOpen(window: Pick<OneTimeMigrationWindow, "removableAfter">, today: string): boolean;
301
321
  /** local 车道上 SQL 独有的几条(M4 / M5 / M8 / M9 / M11 / M17 / M18)的读数(M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
@@ -149,7 +149,7 @@ export const ONE_TIME_MIGRATIONS = [
149
149
  removal: "删模式词读法记号的判据与拒臂:`http/stored-body-replay.ts` 的 `permissionModeReadingUnstated`;`http/checkpoint-ctx.ts` 的 `permissionModeReadingStated` / `permissionModeReadingRefusal` / `PERMISSION_MODE_READINGS`、" +
150
150
  "`StoredBodyReplayRefusal` 的 `permission_mode_reading` 臂及其日志字段与答复臂、`buildCheckpointCtx` 与 `withPermissionModeRewritten` 两处记号写点" +
151
151
  "(后者删后 `resume-legs.ts` `drivePlanReview` 里对它的那一处调用回到只换体的 `{ ...ctx, body }`)、`CheckpointCtx.permissionModeReading`、" +
152
- "`http/stored-body-replay.ts` `storedBodyReplayRefusal` 的 `autonomy` 形参(只服务这一问)与四个问点(`resume-legs.ts` 两处、`approval-sweeps.ts`、`routes/runs.ts`)传 `deps.config.autonomy` 的那一句",
152
+ "`http/stored-body-replay.ts` 里 `StoredReplayDeployment` 的 `autonomy` 位与 `storedBodyReplayRefusal` 读 `deployment.autonomy` 的那一句(这一位只服务这一问;四个问点传 `deps.config` 整份,home 位还服务 `permissions` 一问,不动)",
153
153
  afterRemoval: "没有响亮臂:删码之后仍停驻、在 7.121.x 及以前写下的行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)在设了 " +
154
154
  "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
155
155
  reportKeys: [],
@@ -241,6 +241,38 @@ export const ONE_TIME_MIGRATIONS = [
241
241
  "引擎续跑不受影响:续跑入口读行摘要、不按今日闭集重判停驻行上的这两座,行字节引擎怎么写的还是怎么在",
242
242
  reportKeys: ["retiredCardWordAskRowsRewritten", "retiredCardWordRowsUnreadable"],
243
243
  },
244
+ {
245
+ id: "M19",
246
+ introducedIn: "7.130.0",
247
+ releasedOn: "2026-10-16",
248
+ removableAfter: "2026-11-15",
249
+ removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `StoredToolAxesRereadM19`);" +
250
+ "`boot/resolve-spec.ts` 里递给 `resolveRequestMcp` 的那一实参(`opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)与它的 import;" +
251
+ "`task-mcp.ts` 里 `resolveRequestMcp` / `classifyRequestMcp` / `classifyHonored` / `validateRequestMcpEntries` / `normalizeServer` 的 `storedToolAxes` 形参、" +
252
+ "`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import;`tool-axis-words.ts` 头注与两只谓词 JSDoc 里点名 M19 的那几处;" +
253
+ "`test/mcp-tool-axes-m19.test.ts`(窗关那一格 —— 存量坏形整条不进、warn 与通告点名 `field: toolAxes` —— 改钉到 `test/task-mcp.test.ts`);" +
254
+ "本文件的 `migrationWindowOpen`(若再无别的读者)",
255
+ afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里引擎判官判不过的 `toolAxes`,续跑重放时这一条 server 整条不进," +
256
+ "运维 warn `mcp_injection_dropped`(`reason: malformed_entry`、`field: toolAxes`)+ 用户面通告 `mcp.injection_dropped`,兄弟照挂;停驻在这台 server 工具审批上的任务" +
257
+ "批准后撞 `resume.tool_unavailable`(拒绝照常可出)—— 出路是拒绝那张卡,用改好的 `toolAxes` 重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、" +
258
+ "调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)",
259
+ reportKeys: [],
260
+ },
261
+ {
262
+ id: "M20",
263
+ introducedIn: "7.131.0",
264
+ releasedOn: "2026-10-16",
265
+ removableAfter: "2026-11-15",
266
+ removal: "删 `src/stored-request-hooks-m20.ts` 整只文件(`storedHooksRereadM20` / `rereadStoredHooksM20` / `StoredHooksRereadM20` / `ENTRY_FIELD_NAMES` / `MATCHER_FIELD_NAMES` / `nodeAt` / `cloneDoc` / `removeKeyAt`);" +
267
+ "`boot/resolve-spec.ts` 里递给 `parseTaskSettings` 的重读座实参(`opts?.leg === \"fresh\" || !taskHooksHonored(config) ? {} : { storedHooksReread: storedHooksRereadM20(…) }`)与它的 import、" +
268
+ "`hooks_read_as_before_upgrade` 那一段 warn;`task-settings.ts` 的 `ParseTaskSettingsSeats` / `ParseTaskSettingsResult.hooksReread` / `parseTaskSettings` 第二形参与 hooks 臂里的重读那一支;" +
269
+ "`hooks/hook-runner.ts` 的 `ParseHooksConfigResult.pieces` / `HooksRefusalPiece`(若再无别的读者)与 `hooksCoordinate` 的 export;" +
270
+ "`test/hooks-stored-reread-m20.test.ts`(窗关那一格 —— 整份不采纳 + `server.hooks.resume-reader-refused` 计一笔 —— 改钉到 `test/hooks-entry-admission.test.ts` 的续跑腿组)",
271
+ afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里带这两形的 `settings.hooks`,续跑重放时整份不采纳(其中真在拦的 `PreToolUse` 钩子也不再拦)," +
272
+ "运维 warn `task_settings_deferred`(字段 `hooks`)+ fail-open 登记 `server.hooks.resume-reader-refused`(计数 + 探针 + 一次性 warn,census 第 140 行),任务照常续跑 —— " +
273
+ "出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
274
+ reportKeys: [],
275
+ },
244
276
  ];
245
277
  export function migrationWindowOpen(window, today) {
246
278
  return window.removableAfter === null || today <= window.removableAfter;
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
6
6
  import { signalNumber } from "./host-platform.js";
7
+ import { commandBoundsOf, livenessCutMessage } from "../exec-bounds.js";
7
8
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
8
9
  import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
9
10
  import { createPosixShellFs } from "./posix-shell-fs.js";
@@ -191,7 +192,7 @@ export class RemoteAdbExecutionEnv {
191
192
  w?.();
192
193
  };
193
194
  let lastChunk = Date.now();
194
- const idleMs = options?.readTimeoutMs;
195
+ const liveness = commandBoundsOf({ readTimeoutMs: options?.readTimeoutMs }, undefined).liveness;
195
196
  let paused = false;
196
197
  let onAbort;
197
198
  const teardown = () => {
@@ -262,7 +263,7 @@ export class RemoteAdbExecutionEnv {
262
263
  throw failure;
263
264
  if (finished)
264
265
  break;
265
- const waitMs = idleMs ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
266
+ const waitMs = liveness !== undefined ? Math.max(1, liveness.ms - (Date.now() - lastChunk)) : 30_000;
266
267
  let waitTimer;
267
268
  try {
268
269
  await new Promise((r) => {
@@ -274,8 +275,8 @@ export class RemoteAdbExecutionEnv {
274
275
  if (waitTimer !== undefined)
275
276
  clearTimeout(waitTimer);
276
277
  }
277
- if (idleMs && !finished && Date.now() - lastChunk >= idleMs) {
278
- throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (device hang / disconnected)`);
278
+ if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms) {
279
+ throw new RemoteExecutionError("timeout", livenessCutMessage(liveness));
279
280
  }
280
281
  }
281
282
  while (queue.length)
@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
7
7
  import { signalNumber } from "./host-platform.js";
8
+ import { commandBoundsOf, livenessCutMessage } from "../exec-bounds.js";
8
9
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
9
10
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
10
11
  import { createPosixShellFs } from "./posix-shell-fs.js";
@@ -188,7 +189,7 @@ export class RemoteLocalDockerExecutionEnv {
188
189
  let paused = false;
189
190
  let wake;
190
191
  const signalReady = () => { const w = wake; wake = undefined; w?.(); };
191
- const idleMs = options?.readTimeoutMs;
192
+ const liveness = commandBoundsOf({ readTimeoutMs: options?.readTimeoutMs }, undefined).liveness;
192
193
  const cap = options?.maxOutputBytes;
193
194
  let onAbort;
194
195
  const enqueue = (type, data) => {
@@ -270,7 +271,7 @@ export class RemoteLocalDockerExecutionEnv {
270
271
  throw failure;
271
272
  if (finished)
272
273
  break;
273
- const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
274
+ const waitMs = liveness !== undefined ? Math.max(1, liveness.ms - (Date.now() - lastChunk)) : 30_000;
274
275
  let waitTimer;
275
276
  try {
276
277
  await new Promise((r) => { wake = r; waitTimer = setTimeout(r, waitMs); });
@@ -279,13 +280,13 @@ export class RemoteLocalDockerExecutionEnv {
279
280
  if (waitTimer !== undefined)
280
281
  clearTimeout(waitTimer);
281
282
  }
282
- if (idleMs != null && !finished && Date.now() - lastChunk >= idleMs) {
283
+ if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms) {
283
284
  finished = true;
284
285
  try {
285
286
  child.kill("SIGKILL");
286
287
  }
287
288
  catch { }
288
- throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (command hang)`);
289
+ throw new RemoteExecutionError("timeout", livenessCutMessage(liveness));
289
290
  }
290
291
  }
291
292
  while (queue.length)