@sema-agent/server 7.100.0 → 7.101.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 (102) hide show
  1. package/MIGRATION.md +22 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +80 -9
  5. package/dist/approval-card.d.ts +24 -12
  6. package/dist/approval-card.js +4 -3
  7. package/dist/approval-policy-settlement.d.ts +31 -0
  8. package/dist/approval-policy-settlement.js +16 -0
  9. package/dist/approval-reconciler.d.ts +8 -1
  10. package/dist/approval-reconciler.js +5 -4
  11. package/dist/boot/config-center.js +86 -20
  12. package/dist/boot/execution-env.js +20 -2
  13. package/dist/boot/parked-revive-gate.js +1 -0
  14. package/dist/boot/reapers.js +1 -0
  15. package/dist/boot/resolve-spec.js +25 -8
  16. package/dist/boot/runner-deps.js +9 -3
  17. package/dist/boot/stage-02-registry-pull.js +9 -0
  18. package/dist/boot/stores.js +4 -0
  19. package/dist/config-catalog.js +5 -3
  20. package/dist/config-center/apply-effective.d.ts +25 -0
  21. package/dist/config-center/apply-effective.js +144 -69
  22. package/dist/config-center/apply-ledger.d.ts +12 -2
  23. package/dist/config-center/apply-ledger.js +4 -1
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/http-client.d.ts +4 -1
  27. package/dist/config-center/http-client.js +15 -10
  28. package/dist/config-center/read-warnings.d.ts +115 -6
  29. package/dist/config-center/read-warnings.js +115 -5
  30. package/dist/config-center/types.d.ts +6 -0
  31. package/dist/config-invariants.d.ts +2 -2
  32. package/dist/config-invariants.js +15 -0
  33. package/dist/config-provider.d.ts +4 -0
  34. package/dist/config-provider.js +64 -9
  35. package/dist/config-types.d.ts +37 -8
  36. package/dist/config.js +87 -22
  37. package/dist/deployment-governance.d.ts +45 -12
  38. package/dist/deployment-governance.js +37 -9
  39. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  40. package/dist/env-name-allowlist-knobs.js +24 -0
  41. package/dist/fleet-client.d.ts +2 -1
  42. package/dist/hooks/hook-llm.js +13 -4
  43. package/dist/host-lsp-manager.d.ts +14 -2
  44. package/dist/host-lsp-manager.js +7 -6
  45. package/dist/http/admission.js +22 -7
  46. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  47. package/dist/http/memory-store-refusal-reply.js +10 -0
  48. package/dist/http/routes/approvals-assistant.js +1 -0
  49. package/dist/http/routes/capabilities.js +3 -3
  50. package/dist/http/routes/memory-bundle.js +3 -0
  51. package/dist/http/routes/memory-compliance.js +3 -5
  52. package/dist/http/routes/memory-origin.js +3 -5
  53. package/dist/http/routes/sessions.js +7 -6
  54. package/dist/http/routes/side-query.js +10 -4
  55. package/dist/instruction-source-notice.d.ts +97 -0
  56. package/dist/instruction-source-notice.js +78 -0
  57. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  58. package/dist/memory-layer-legacy-lock.js +74 -0
  59. package/dist/memory-operator-faces.js +1 -17
  60. package/dist/memory-store-refusal.d.ts +37 -0
  61. package/dist/memory-store-refusal.js +14 -0
  62. package/dist/model-entry-refusal.d.ts +130 -0
  63. package/dist/model-entry-refusal.js +157 -0
  64. package/dist/model-provider.d.ts +9 -3
  65. package/dist/model-route-endpoint.d.ts +1 -1
  66. package/dist/model-select.d.ts +49 -4
  67. package/dist/model-select.js +48 -17
  68. package/dist/observability/fail-open.d.ts +9 -1
  69. package/dist/observability/fail-open.js +9 -1
  70. package/dist/observability/metrics.js +1 -0
  71. package/dist/observability/secret-env-scrub.d.ts +27 -4
  72. package/dist/observability/secret-env-scrub.js +22 -7
  73. package/dist/plugins/caching-session-store.d.ts +6 -0
  74. package/dist/plugins/caching-session-store.js +6 -0
  75. package/dist/plugins/local-session-store.d.ts +20 -25
  76. package/dist/plugins/local-session-store.js +111 -64
  77. package/dist/plugins/pg-session-storage.d.ts +7 -0
  78. package/dist/plugins/pg-session-storage.js +19 -0
  79. package/dist/plugins/remote-env-host.d.ts +3 -1
  80. package/dist/plugins/remote-env-host.js +3 -6
  81. package/dist/plugins/tidb-pool.js +5 -0
  82. package/dist/plugins/tidb-session-store.d.ts +13 -0
  83. package/dist/plugins/tidb-session-store.js +14 -0
  84. package/dist/plugins/web-search.d.ts +2 -1
  85. package/dist/plugins/web-search.js +43 -24
  86. package/dist/project-memory.d.ts +10 -0
  87. package/dist/project-memory.js +28 -14
  88. package/dist/run-local.d.ts +17 -15
  89. package/dist/run-local.js +115 -15
  90. package/dist/sealed-key.d.ts +5 -2
  91. package/dist/server-secret-env.d.ts +23 -1
  92. package/dist/server-secret-env.js +9 -0
  93. package/dist/tool-approval.d.ts +65 -31
  94. package/dist/tool-approval.js +96 -49
  95. package/dist/trace/core-keyset-guard.d.ts +27 -5
  96. package/dist/trace/engine-notice-wire.d.ts +27 -1
  97. package/dist/trace/engine-notice-wire.js +8 -0
  98. package/dist/trace/wire-projection-faces.d.ts +2 -2
  99. package/dist/trace/wire-projection-faces.js +2 -2
  100. package/package.json +2 -2
  101. package/dist/memory-layer-preflight.d.ts +0 -46
  102. package/dist/memory-layer-preflight.js +0 -121
@@ -0,0 +1,130 @@
1
+ /** 不可用的成因 —— **闭集**(加一个成因 ⇒ {@link MODEL_ENTRY_REFUSAL_CODES} 与 {@link refusalEventOf} 编译红)。 */
2
+ export type ModelEntryRefusalReason = "no_brain" | "entry_invalid";
3
+ /** 机读码(拼法随 `config.*` 家族,按**规则**命名):出现在拒因句的方括号里、warn 字段里、`modelRefusals` 里。 */
4
+ export declare const MODEL_ENTRY_REFUSAL_CODES: {
5
+ readonly no_brain: "config.model_no_brain";
6
+ readonly entry_invalid: "config.model_entry_invalid";
7
+ };
8
+ export type ModelEntryRefusalCode = (typeof MODEL_ENTRY_REFUSAL_CODES)[ModelEntryRefusalReason];
9
+ /** 「这张表没有缺省模型」的机读码(S-705;boot 拒启与热路径模型面组拒同一个码)。 */
10
+ export declare const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
11
+ /** 🔴 S-712「被拒的名字被档位 / 别名增广铸到了别的条目上(或读不出被丢条目的名字,无从证明没有)」的机读码
12
+ * (不变量 `refused-names-not-minted`;热路径 / boot 首次 apply / dry-run 同一个码)。 */
13
+ export declare const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
14
+ /**
15
+ * 一条不可用条目的**事实**(纯数据;`config.modelRefusals[<name>]` 的值)。
16
+ * 🔴 不含任何值:不含 baseUrl、不含钥匙、不含坏字段的原值 —— 只有成因、码、上游 id(显式选择按 id 形也要命中)、
17
+ * 坏字段的**键名**,与一句给运维的出路。
18
+ */
19
+ export interface ModelEntryRefusal {
20
+ readonly reason: ModelEntryRefusalReason;
21
+ readonly code: ModelEntryRefusalCode;
22
+ /** 条目的上游 `id`(壳常按 id 形回读 mainLoopModel —— 按 id 选中同样要拿到拒因,不许落「未知模型」)。 */
23
+ readonly id: string;
24
+ /** 条目写的 provider 词(`no_brain` 的主语;`entry_invalid` 时照录,词可能本身就是坏的那一处)。 */
25
+ readonly provider: string;
26
+ /** `entry_invalid`:不合形的字段键路径(`contextWindow` / `cost.input` / `api` …);`no_brain` 恒空。 */
27
+ readonly fields: readonly string[];
28
+ /** 一句话:为什么不可用 + 出路(不含值)。 */
29
+ readonly detail: string;
30
+ }
31
+ /** 判据要知道的**部署事实**(不在条目里)。 */
32
+ export interface CatalogEntryVerdictCtx {
33
+ /** anthropic 路由装上了没有(= `config.anthropic` 在场,`createBrain` 同判)。 */
34
+ readonly anthropicRouteMounted: boolean;
35
+ }
36
+ /** 条目最小读面(`CenterModel` 与本地投影都结构满足;形判走 settings-schema,不信这张型)。 */
37
+ export interface CatalogEntryLike {
38
+ readonly name: string;
39
+ readonly id: string;
40
+ readonly provider: string;
41
+ }
42
+ /**
43
+ * **逐条判据**:`undefined` = 本部署能完整服务它。判序即语义:先形(不合形的条目谈不上走哪条腿),再腿。
44
+ *
45
+ * 形判的属主是 settings-schema 的 `ModelEntry`(两条读腿用的就是它)—— 本仓不手抄任何一条字段规则;这里只是
46
+ * 在第二道边界(绕过读腿的输入)上**再问一次同一个属主**,并把答案变成一条带码的拒因。只取 `success` 与 issue
47
+ * 坐标,不取它的输出(输出会补缺省、剥扩展键,那是读腿的事)。
48
+ */
49
+ export declare function judgeCatalogEntry(m: CatalogEntryLike, ctx: CatalogEntryVerdictCtx): ModelEntryRefusal | undefined;
50
+ /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及身份证明不了的被丢条目(坐标)。 */
51
+ export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
52
+ readonly usable: readonly M[];
53
+ readonly refused: Readonly<Record<string, ModelEntryRefusal>>;
54
+ /** S-712:读腿逐条丢掉、而身份证明不了的条目坐标(`models.3`;载体上不带 `name` 的那些 —— 读腿只在原文档对不上本次解析时
55
+ * 这样交出,见 `read-warnings.ts` `DroppedModelIdentity`)。非空 ⇒ 不变量 `refused-names-not-minted` 无从成立 ⇒ 模型面组拒。 */
56
+ readonly unreadable: readonly string[];
57
+ }
58
+ /**
59
+ * 批量形(模型面 stage 与 dry-run 共用这一只 —— 报数与真 apply 永不撕裂)。
60
+ * S-712:`dropped` = 读腿交出的被丢条目身份(`EffectiveConfig.models.droppedIdentities`)—— 它们与逐条判据判出的 `entry_invalid`
61
+ * 是**同一件事**(条目不合 `ModelEntry` 形),只是在第一道边界就被丢了;带名字的铸墓碑,不带名字的(读腿证明不了身份)进
62
+ * `unreadable`。没有字符串名的坏行读腿已按停用处置、根本不进载体(7.101.0 验修轮 V-2:没有名字就没有可保留的名字)。
63
+ * 入参按 `unknown` 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改),形不对的一律按证明不了处置(fail-closed)。
64
+ * 与一条可用 / 已判不可用条目同名时不覆盖(可用条目仍可用;同名撞车由不变量 `refused-names-not-minted` 响亮拒)。
65
+ */
66
+ export declare function judgeCatalogEntries<M extends CatalogEntryLike>(enabled: readonly M[], ctx: CatalogEntryVerdictCtx, dropped?: unknown): CatalogEntriesVerdict<M>;
67
+ /** 成因 → 它那一条 warn 的事件名(**穷举**)。两个名字是票面点名的既有/新事件,运维的告警规则按名挂。 */
68
+ export declare function refusalEventOf(reason: ModelEntryRefusalReason): "sema_registry_model_no_brain" | "sema_registry_model_entry_invalid";
69
+ /** warn 字段(不含值):`field` = 第一处坏字段(告警规则最常按它分组),`fields` = 全部。 */
70
+ export declare function refusalWarnFields(name: string, r: ModelEntryRefusal): Record<string, unknown>;
71
+ /**
72
+ * 一次**显式选择**命中了不可用条目时的拒因句(HTTP `request.model_unavailable` 的 `error`、run-local 的 stderr、
73
+ * hook 选模型的失败文案 —— 各处同一句)。`field` = 调用方的通道名(`model` / `compactionModel` / `agents[0].model` /
74
+ * `@-mention`),`requested` = 调用方发来的原形(name 或 id),已由调用方截断。
75
+ */
76
+ export declare function modelUnavailableMessage(field: string, requested: string, name: string, r: ModelEntryRefusal): string;
77
+ /** 缺省模型不变量读的切面(`ServiceConfig` 与 `CrossInvariantView` 都结构满足)。 */
78
+ export interface DefaultModelView {
79
+ readonly models: Readonly<Record<string, unknown>>;
80
+ readonly modelRefusals: Readonly<Record<string, ModelEntryRefusal>>;
81
+ }
82
+ /**
83
+ * 这张表是从哪条腿来的(闭集 —— 拒因句的出路按腿分:加腿不补 {@link defaultModelRefusal} 的臂 = 编译红)。
84
+ * · `env` —— 进程此刻服务的表**没有目录在管**(没配目录 / 目录迟到且无 LKG / 目录的模型面这一组被拒):`MODEL_ID` 是出路之一;
85
+ * · `catalog` —— 判的是一份**目录候选**(模型面 stage):目录一旦生效就整张换掉 env 表,`MODEL_ID` 救不了这份候选 ——
86
+ * 出路只在目录里(修条目 / 换绑)。opus 腿 ④(7.101.0 合并复审):此前两腿同一句,目录腿教「设 MODEL_ID 即可」是假话。
87
+ */
88
+ export type ModelTableLeg = "env" | "catalog";
89
+ /**
90
+ * 🔴 S-705 —— **这张模型表有没有一只缺省模型**(`models.default`)。`undefined` = 有。
91
+ *
92
+ * 规则只有一句:**进程服务的那张表必须有 `default`**。env 腿(`MODEL_ID`)与目录腿(缺省解析)是它的两个来源,
93
+ * 任一在场即过;目录一旦生效就整张换掉 env 表,所以判的永远是「生效之后的那张表」—— 这正是它必须在目录应用
94
+ * **之后**判的理由(此前 `parseModelDomain` 在目录应用之前就对 `MODEL_ID` 空串拒启,把只配了目录的用户无谓拒掉)。
95
+ *
96
+ * 拒因一句:码 + 这条腿上**真实**的出路 + 若目录在场但每条都不可用,点名它们与各自的码。env 腿句首保留 `MODEL_ID is required`
97
+ * 这个既有的可 grep 片段(壳与运维脚本认它);目录腿不提 `MODEL_ID`(见 {@link ModelTableLeg})。
98
+ */
99
+ export declare function defaultModelRefusal(view: DefaultModelView, leg: ModelTableLeg): string | undefined;
100
+ /** 一个被铸出的被拒名字:`name` = 被拒条目的名字,`target` = 增广把它指向的那条条目,`via` = 它是以什么身份被铸出的。 */
101
+ export interface MintedRefusedName {
102
+ readonly name: string;
103
+ readonly target: string;
104
+ /** `tier-word`(档位词)/ `cc-alias`(CC 别名 → 它的档位)/ `best`(头档别名)/ `catalog-entry`(一条可用条目与它同名)。 */
105
+ readonly via: string;
106
+ /** 被拒条目自己的拒因码。 */
107
+ readonly code: ModelEntryRefusalCode;
108
+ }
109
+ /** 不变量 `refused-names-not-minted` 读的切面(`ServiceConfig` 与 `CrossInvariantView` 都结构满足)。 */
110
+ export interface RefusedNameView {
111
+ readonly models: Readonly<Record<string, unknown>>;
112
+ readonly tiers: Readonly<Record<string, string>>;
113
+ readonly modelRefusals: Readonly<Record<string, ModelEntryRefusal>>;
114
+ }
115
+ /**
116
+ * 🔴 S-712 —— 不变量 `refused-names-not-minted` 的判据:**这张表回应的每一个名字(目录键 ∪ core `expandTiers` 增广出来的档位词 /
117
+ * CC 别名 / `best`)都不是一个被拒条目的名字**。返回撞上的那些(空 = 成立)。
118
+ *
119
+ * 为什么是「增广之后」:引擎在构造 / 换装时自己 `expandTiers(models, tiers)`,显式 `model:"sonnet"` 与角色绑定 `advisor→sonnet` 都在
120
+ * 增广目录上解析。一条条目被拒(不可用 / 被 schema 丢掉)之后它的名字空出来,同名的档位词 / CC 别名就不再被它 shadow,增广把
121
+ * 那个词铸到另一条条目上 —— 点它的请求被静默换了目的地、端点与账号(gpt-6 腿 F2 / F3,真复现)。判「增广之后」让引擎拿到的目录
122
+ * **永远没有**被拒名:显式点它落 `request.model_unavailable`(选择面让出名字 + 查拒因表),角色绑定落引擎对未知名的响亮拒。
123
+ * 可用条目与被拒条目同名(同一个名字发布了两条、其中一条坏的)同样算撞:哪一条才是运维的意思无从判,拒比猜安全。
124
+ */
125
+ export declare function mintedRefusedNames(view: RefusedNameView): MintedRefusedName[];
126
+ /** 不变量 `refused-names-not-minted` 的拒因句(`undefined` = 成立)。warn 与世代账 / 拒因同一句;逐对明细另走 {@link mintedRefusedNames}。 */
127
+ export declare function refusedNameMintedRefusal(view: RefusedNameView): string | undefined;
128
+ /** 身份证明不了的被丢条目的拒因句(同一不变量同一个码:证明不了的名字无从证明它不被增广铸出)。 */
129
+ export declare function unreadableDroppedRefusal(entries: readonly string[]): string;
130
+ //# sourceMappingURL=model-entry-refusal.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { CC_MODEL_TIER_ALIASES, DEFAULT_TIER_ORDER, expandTiers } from "@sema-agent/core";
2
+ import { ModelEntry } from "@sema-agent/settings-schema";
3
+ import { catalogBrainLane } from "./model-provider.js";
4
+ import { describeSchemaIssueFields, describeSchemaIssues } from "./config-center/read-warnings.js";
5
+ import { DEFAULT_MODEL_REF } from "./model-select.js";
6
+ export const MODEL_ENTRY_REFUSAL_CODES = {
7
+ no_brain: "config.model_no_brain",
8
+ entry_invalid: "config.model_entry_invalid",
9
+ };
10
+ export const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
11
+ export const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
12
+ function laneMount(lane, ctx) {
13
+ switch (lane) {
14
+ case "gateway":
15
+ return { mounted: true, how: "" };
16
+ case "anthropic":
17
+ return { mounted: ctx.anthropicRouteMounted, how: "ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN) in this service's env" };
18
+ }
19
+ }
20
+ export function judgeCatalogEntry(m, ctx) {
21
+ const shape = ModelEntry.safeParse(m);
22
+ if (!shape.success) {
23
+ const fields = describeSchemaIssueFields(shape.error);
24
+ return entryInvalidRefusal(typeof m.id === "string" ? m.id : "", typeof m.provider === "string" ? m.provider : "", fields, describeSchemaIssues(shape.error) ?? fields.join(", "));
25
+ }
26
+ const lane = catalogBrainLane(m.provider);
27
+ const mount = laneMount(lane, ctx);
28
+ if (!mount.mounted) {
29
+ return {
30
+ reason: "no_brain",
31
+ code: MODEL_ENTRY_REFUSAL_CODES.no_brain,
32
+ id: m.id,
33
+ provider: m.provider,
34
+ fields: [],
35
+ detail: `provider "${m.provider}" rides the ${lane} brain, which is not mounted on this deployment — the entry is not served here ` +
36
+ `(it is never routed to another brain with its own key); set ${mount.how}, ` +
37
+ 'or change the entry\'s provider to an OpenAI-compatible route ("gateway" / "vllm") with its own baseUrl',
38
+ };
39
+ }
40
+ return undefined;
41
+ }
42
+ function entryInvalidRefusal(id, provider, fields, issues) {
43
+ return {
44
+ reason: "entry_invalid",
45
+ code: MODEL_ENTRY_REFUSAL_CODES.entry_invalid,
46
+ id,
47
+ provider,
48
+ fields: [...fields],
49
+ detail: `the catalog entry does not match the settings-schema ModelEntry shape (${issues}) — it is dropped from this ` +
50
+ "deployment's catalog on its own (the other entries keep serving) instead of being served with a value nothing validated; fix the entry to bring it back",
51
+ };
52
+ }
53
+ export function judgeCatalogEntries(enabled, ctx, dropped) {
54
+ const usable = [];
55
+ const refused = {};
56
+ for (const m of enabled) {
57
+ const r = judgeCatalogEntry(m, ctx);
58
+ if (r === undefined)
59
+ usable.push(m);
60
+ else
61
+ refused[String(m.name)] = r;
62
+ }
63
+ const unreadable = [];
64
+ for (const d of Array.isArray(dropped) ? dropped : []) {
65
+ const e = (typeof d === "object" && d !== null ? d : {});
66
+ const where = typeof e.entry === "string" ? e.entry : "(unknown coordinate)";
67
+ if (typeof e.name !== "string") {
68
+ unreadable.push(where);
69
+ continue;
70
+ }
71
+ if (Object.hasOwn(refused, e.name))
72
+ continue;
73
+ const fields = Array.isArray(e.fields) ? e.fields.filter((f) => typeof f === "string") : [];
74
+ refused[e.name] = entryInvalidRefusal(typeof e.id === "string" ? e.id : "", typeof e.provider === "string" ? e.provider : "", fields, typeof e.issues === "string" ? e.issues : fields.join(", "));
75
+ }
76
+ return { usable, refused, unreadable };
77
+ }
78
+ export function refusalEventOf(reason) {
79
+ switch (reason) {
80
+ case "no_brain":
81
+ return "sema_registry_model_no_brain";
82
+ case "entry_invalid":
83
+ return "sema_registry_model_entry_invalid";
84
+ }
85
+ }
86
+ export function refusalWarnFields(name, r) {
87
+ return {
88
+ model: name,
89
+ provider: r.provider,
90
+ code: r.code,
91
+ ...(r.fields.length > 0 ? { field: r.fields[0], fields: [...r.fields] } : {}),
92
+ hint: r.detail,
93
+ };
94
+ }
95
+ export function modelUnavailableMessage(field, requested, name, r) {
96
+ const alias = requested === name ? "" : ` (catalog entry "${name}")`;
97
+ return `${field} "${requested}"${alias} is published in the catalog but unavailable on this deployment [${r.code}] — ${r.detail}`;
98
+ }
99
+ export function defaultModelRefusal(view, leg) {
100
+ if (Object.hasOwn(view.models, DEFAULT_MODEL_REF) && view.models[DEFAULT_MODEL_REF] !== undefined)
101
+ return undefined;
102
+ const refused = Object.entries(view.modelRefusals).map(([n, r]) => `"${n}" [${r.code}]`);
103
+ const unavailable = refused.length > 0 ? ` (every enabled catalog entry is unavailable on this host: ${refused.join(", ")})` : "";
104
+ switch (leg) {
105
+ case "env":
106
+ return (`MODEL_ID is required when no catalog default resolves [${DEFAULT_MODEL_MISSING_CODE}] — this model table has no default model` +
107
+ unavailable +
108
+ ". Set MODEL_ID=<model name your gateway serves>, or enable at least one model this host can serve in the catalog " +
109
+ "(config.d/models.json, or the config center) — either one is enough (a config center that answers after the boot budget " +
110
+ "counts as absent at boot: raise CONFIG_BOOT_FETCH_BUDGET_MS to wait for it).");
111
+ case "catalog":
112
+ return (`this catalog candidate has no default model [${DEFAULT_MODEL_MISSING_CODE}]` +
113
+ unavailable +
114
+ " — fix the named entries (each has its own warn with the reason) or enable / bind a default this host can serve, in the catalog itself " +
115
+ "(a catalog in force replaces the env model table, so MODEL_ID cannot stand in for it).");
116
+ }
117
+ }
118
+ function mintedVia(name, own, tiers) {
119
+ if (own)
120
+ return "catalog-entry";
121
+ if (Object.hasOwn(CC_MODEL_TIER_ALIASES, name))
122
+ return `cc-alias of tier "${CC_MODEL_TIER_ALIASES[name]}"`;
123
+ if (name === "best")
124
+ return "best-tier alias";
125
+ if (DEFAULT_TIER_ORDER.includes(name) || Object.hasOwn(tiers, name))
126
+ return "tier word";
127
+ return "expansion key";
128
+ }
129
+ export function mintedRefusedNames(view) {
130
+ const refusedNames = Object.keys(view.modelRefusals);
131
+ if (refusedNames.length === 0)
132
+ return [];
133
+ const expanded = (expandTiers(view.models, view.tiers) ?? view.models);
134
+ const out = [];
135
+ for (const name of refusedNames) {
136
+ if (!Object.hasOwn(expanded, name) || expanded[name] === undefined)
137
+ continue;
138
+ const t = expanded[name]?.name;
139
+ out.push({ name, target: typeof t === "string" && t.length > 0 ? t : name, via: mintedVia(name, Object.hasOwn(view.models, name), view.tiers), code: view.modelRefusals[name].code });
140
+ }
141
+ return out;
142
+ }
143
+ export function refusedNameMintedRefusal(view) {
144
+ const minted = mintedRefusedNames(view);
145
+ if (minted.length === 0)
146
+ return undefined;
147
+ const pairs = minted.map((m) => `"${m.name}" [${m.code}] → "${m.target}" (${m.via})`);
148
+ return (`unavailable catalog entries would be re-minted onto other entries by the tier/alias expansion [${REFUSED_NAME_MINTED_CODE}]: ${pairs.join(", ")} — ` +
149
+ "an explicit pick of that name (or a role bound to it) would silently run on the other entry's endpoint and account; " +
150
+ "rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
151
+ }
152
+ export function unreadableDroppedRefusal(entries) {
153
+ return (`catalog entries dropped by the settings-schema read could not be identified [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
154
+ "the raw document could not be matched to the snapshot that was parsed (e.g. the local file changed between two reads), so the candidate cannot prove the tier/alias expansion does not re-mint their names; " +
155
+ "the next read re-judges (fix the named entries if this persists).");
156
+ }
157
+ //# sourceMappingURL=model-entry-refusal.js.map
@@ -69,9 +69,11 @@ export declare function modelBrainLane(provider: ModelProvider): ModelBrainLane;
69
69
  * unrecognized provider)" 是它的原话)。本函数做的只是把那句散文变成**一个有名字的判词**,于是
70
70
  * 「目录里写了个没人认识的 provider」这件事在代码里有一处可指的坐标,而不是散在两个 `=== "anthropic"` 里。
71
71
  *
72
- * ⚠️ **未闭环**(S-461 收车档登记):目录词今天仍然**无声**地落进网关腿。要让它也响亮,需要的是一条
73
- * **热路径上的**告警/拒收(config-center 应用期逐条校验 + 运维可读的拒因),那是另一件事、另一台车 ——
74
- * 在这里加一个 throw 会让一次热发布打死在跑的副本,方向反了。
72
+ * ⚠️ **未闭环(收窄后如实)**:认不出的目录词今天仍然**无声**地落进网关腿(OpenAI 兼容;条目带自己的 baseUrl /
73
+ * 钥匙,打的是它自己的端点 —— 不是凭据越界,是词面失真)。S-704(7.101.0)已把**热路径上的逐条判据**落地
74
+ * (`model-entry-refusal.ts`:本部署没有对应 brain 的条目整条不可用 —— 今天那一形是「anthropic 词 + 本机没装
75
+ * anthropic 路由」),但**域外词**是否也该不可用是另一个问题(web-admin 的新条目缺省就写 `"openai"`,预设写厂商名;
76
+ * 把它们判不可用会让大量在跑的目录整条消失),另车带三问定。在这里加一个 throw 会让一次热发布打死在跑的副本,方向反了。
75
77
  */
76
78
  export declare function catalogBrainLane(provider: string | undefined): ModelBrainLane;
77
79
  /**
@@ -85,6 +87,10 @@ export declare function catalogBrainLane(provider: string | undefined): ModelBra
85
87
  * 所以「这条路由要不要一个 openai 端点」的判据必须同时看词与装配:词判 {@link catalogBrainLane},
86
88
  * 装配判路由在不在。本函数是这一合取的**唯一**写法 —— 端点判官(`model-route-endpoint.ts`)、降级腿与
87
89
  * 启动摘要(`brain.ts`)都调它,不再各写一个 `=== "anthropic" && config.anthropic`。
90
+ *
91
+ * 🔴 S-704(7.101.0):**目录条目**已到不了「anthropic 词、路由没装」这一格 —— 模型面 stage 的逐条判据把它判成不可用
92
+ * (`config.model_no_brain`,不进 `config.models`)。这一格今天只剩 **env 腿**:`MODEL_PROVIDER=anthropic` 而没配 anthropic
93
+ * 凭据的部署,env 模型落网关腿、用的是网关自己的 `MODEL_API_KEY`(凭据与端点同属网关,不越界;是否也该拒启另车)。
88
94
  */
89
95
  export declare function dispatchBrainLane(provider: string | undefined, anthropicRouteMounted: boolean): ModelBrainLane;
90
96
  //# sourceMappingURL=model-provider.d.ts.map
@@ -17,7 +17,7 @@
17
17
  * ## 执法点(判据只在这里,执法点在调用处 —— 与 S-475 `gateway-url-userinfo` 同形)
18
18
  * · boot:`config.ts` 九域解析之后的跨域不变量区(锚 `CROSS-INVARIANT:model-route-endpoint`)⇒ 拒启;严格名册
19
19
  * 布防(`isRosterGateArmed`)时 env 腿是占位、从不被派发 ⇒ 不判它(备用网关那一条照判);
20
- * · 热路径:`config-center/apply-effective.ts` 的模型面 stage ⇒ 候选整批拒、上一代继续服务;
20
+ * · 热路径:`config-center/apply-effective.ts` 的模型面 stage ⇒ 模型面组拒(opus 腿 O3:同候选其余组照落)、上一代模型面继续服务;
21
21
  * · run-local:配置源解析完之后按真要派发的表再判一次(`run-local.ts`,7.98.0 合并复审 F2)—— 本入口没有名册门,
22
22
  * loadConfig 对严格名册布防的占位豁免在这里兑现不了;没布防时恒惰性;
23
23
  * · 登记:`config-invariants.ts` 的 `model-route-endpoint` 条(表的「不许有表外不变量」职责);
@@ -1,3 +1,4 @@
1
+ import type { ModelEntryRefusal } from "./model-entry-refusal.js";
1
2
  /**
2
3
  * Resolve the model ref for a task spec from the request (CLI `/model` picker + `run --model`).
3
4
  *
@@ -27,12 +28,53 @@
27
28
  * `resolveModel("default", catalog)` 解它)。**单点**:读面要说「这台部署的主模型是谁」时读同一个词
28
29
  * (`auto-mode-face.ts` 的席解析器入参),两处各写一个 `"default"` 字面串正是漂移的成因。 */
29
30
  export declare const DEFAULT_MODEL_REF = "default";
30
- export declare function resolveTaskModel(bodyModel: unknown, objective: string, catalog: Record<string, unknown>, allowlist?: readonly string[]): {
31
+ export declare function resolveTaskModel(bodyModel: unknown, objective: string, catalog: Record<string, unknown>, allowlist?: readonly string[], refusals?: Readonly<Record<string, ModelEntryRefusal>>): {
31
32
  model: string;
32
33
  cleanedObjective: string;
33
34
  unknownExplicit?: string;
34
35
  notAllowed?: ModelNotAllowed;
36
+ unavailable?: ModelUnavailable;
35
37
  };
38
+ /**
39
+ * S-704 判别式拒因:这次**用户选择**点中的是一条已发布、但本部署不可用的目录条目(`config.modelRefusals`)。
40
+ * 与 {@link ModelNotAllowed} 同一种处置口:本函数不 throw,调用点按 leg 分流(fresh 400 `request.model_unavailable` /
41
+ * resume 留声降级)。拒因句由 `model-entry-refusal.ts` 的 `modelUnavailableMessage` 铸(一处)。
42
+ */
43
+ export interface ModelUnavailable {
44
+ /** 调用方发来的原始 ref(name / id / `@name`),已做过日志安全的 repr 归一。 */
45
+ requested: string;
46
+ /** 命中的不可用条目的 catalog name。 */
47
+ name: string;
48
+ refusal: ModelEntryRefusal;
49
+ /** 走的是哪条用户通道:`explicit`=body.model/settings.model,`mention`=objective 内联 `@name`。 */
50
+ source: "explicit" | "mention";
51
+ }
52
+ /**
53
+ * S-704 —— **用户可选目录**:服务的目录经 core `expandTiers` 增广(档位词 / CC 别名 / `best`),再把**不可用条目的名字**
54
+ * 从增广出来的别名键里**让出**。全部用户**显式**选择通道(body.model / `@name` / settings.model / compactionModel /
55
+ * agents[].model / side-query 的 body.model / 名单判定 / `/v1/models` 的可点性)都在这一只上解析 —— 一个优先序,不各写一份;
56
+ * 与之配套的另一半是 {@link matchCatalogModel} 的「保留名不走 id 回退」。
57
+ * ⚠️ 射程只到显式选择:引擎自己的隐式选路(角色的档位缺省、档位梯、workflow 名单在 core 里的解析)用的是 core 的增广目录,
58
+ * 本函数**不**进那里(codex 验修轮 V4:让出名字会改变只带 `modelRole` 的隐式选路)。不可用条目名撞上别名词时引擎侧的改绑
59
+ * (运维配置里点名它的角色绑定 / workflow 名单)自 S-712 起由不变量 `refused-names-not-minted`(`model-entry-refusal.ts`)在
60
+ * 源头收口:增广会把被拒名铸到别的条目上的候选,模型面整组被拒、进不了引擎 —— 于是本函数的「让出」在已采纳的配置上恒是
61
+ * 空操作,保留作 HTTP 选择面的第二道(绕过 stage 的写者若造出这种表,选择面仍不改绑)。
62
+ *
63
+ * 为什么要让出(codex r1 [high],真复现):一条已发布的条目占着它的名字;它可用时,core 的别名展开**让它**(`shadowed`,
64
+ * 同名别名不生成)。它因不可用被摘出目录之后,展开会把同名的别名(条目恰好叫 `sonnet` / `pro` …)重新指到另一只模型 ——
65
+ * 一次点它的选择被静默改绑到另一只模型(及其端点、账号),一张写着它的 @-名单也随之准了那一只。让出之后,它的名字在可选
66
+ * 目录里不存在,选择走 {@link refusedCatalogEntry} 拿到拒因码,名单里的它解析不到任何条目(准不了任何东西)。
67
+ * 可用条目的真名永远不会被让出:目录里 name 唯一,不可用条目的名字只可能撞上**展开生成**的别名键。
68
+ */
69
+ export declare function selectableCatalog<M>(models: Record<string, M>, tiers: Record<string, string> | undefined, refusals: Readonly<Record<string, ModelEntryRefusal>> | undefined): Record<string, M>;
70
+ /**
71
+ * S-704 —— ref(name 或上游 id)→ 它命中的**不可用条目**;未命中 ⇒ undefined。与 {@link matchCatalogModel} 同一种双键
72
+ * 索引(name 用 `Object.hasOwn`,id 扫值)。调用方**先**查可用目录、再查这里(一个 id 被可用与不可用条目共享时,可用那条胜)。
73
+ */
74
+ export declare function refusedCatalogEntry(ref: string, refusals: Readonly<Record<string, ModelEntryRefusal>> | undefined): {
75
+ name: string;
76
+ refusal: ModelEntryRefusal;
77
+ } | undefined;
36
78
  /** [ref] 判别式拒因:名单在场且这次**用户选择**落在名单外。`resolved` 已是目录 name 形(归一后)。 */
37
79
  export interface ModelNotAllowed {
38
80
  /** 调用方发来的原始 ref(name / 档位词 / 别名 / id / `@name`),已做过日志安全的 repr 归一。 */
@@ -64,7 +106,7 @@ export interface ModelNotAllowed {
64
106
  * ⚠️ 射程:本门是**提交时**门。core 运行时路由(degrade 降级链 / cascade 梯 / roles 解析)按 operator
65
107
  * 配置自己走,**不经本门**——那不是一次用户选择,如实声明,不假装是运行时执法。
66
108
  */
67
- export declare function isModelAllowlisted(ref: string, catalog: Record<string, unknown>, allowlist: readonly string[] | undefined): boolean;
109
+ export declare function isModelAllowlisted(ref: string, catalog: Record<string, unknown>, allowlist: readonly string[] | undefined, refusals?: Readonly<Record<string, ModelEntryRefusal>>): boolean;
68
110
  /**
69
111
  * [ref] —— ref → **终端 catalog name**(拒因/日志的可读形)。
70
112
  *
@@ -73,15 +115,18 @@ export declare function isModelAllowlisted(ref: string, catalog: Record<string,
73
115
  * 取条目自己的 `Model.name`(applyEffective 建目录时 `models[m.name].name === m.name`,env lane 同)
74
116
  * 才是终端名;拿不到时如实退回匹配键(诚实缺席,不猜)。
75
117
  */
76
- export declare function terminalCatalogName(ref: string, catalog: Record<string, unknown>): string | undefined;
118
+ export declare function terminalCatalogName(ref: string, catalog: Record<string, unknown>, refusals?: Readonly<Record<string, ModelEntryRefusal>>): string | undefined;
77
119
  /**
78
120
  * [ref]② name+id 双键目录索引:返回 ref 命中的目录 **name 键**(spec.model 的规范形),未知 → undefined。
79
121
  * - name 命中用 Object.hasOwn(🔴 不用真值索引:Object.prototype 继承键 "constructor"/"__proto__" 等在裸
80
122
  * 索引下是真值,会跳过 allow-list——沿袭原实现的纪律)。
81
123
  * - id 命中扫目录值(`Model.id`),跳过 "default" 别名键(它与其指向的真身共享 id,解析到别名会让 /model
82
124
  * 面板与实际路由再度撕裂);同 id 多 name 时取目录序第一个(与 /v1/models 列表序一致,确定性)。
125
+ * - 🔴 S-704(codex 验修轮 V1):**名字先于 id** 这条序对不可用条目同样成立 —— `refusals` 里的名字是一条已发布
126
+ * 条目的名字,它不在可用目录里时**不许**被 id 回退解析成另一条可用条目(v7.100.0 那条条目在目录里,name 命中先返回
127
+ * 它自己)。命中保留名 ⇒ undefined,调用方接着问 {@link refusedCatalogEntry} 拿拒因码。
83
128
  */
84
- export declare function matchCatalogModel(ref: string, catalog: Record<string, unknown>): string | undefined;
129
+ export declare function matchCatalogModel(ref: string, catalog: Record<string, unknown>, refusals?: Readonly<Record<string, ModelEntryRefusal>>): string | undefined;
85
130
  /**
86
131
  * vision precheck: a catalog model can read images iff its `input` modality list
87
132
  * includes `"image"` (the SAME test as the vision capability on GET /v1/models — `server.ts`). FALSE-SAFE on
@@ -1,31 +1,60 @@
1
- import { parseModelMention } from "@sema-agent/core";
1
+ import { expandTiers, parseModelMention } from "@sema-agent/core";
2
2
  export const DEFAULT_MODEL_REF = "default";
3
- export function resolveTaskModel(bodyModel, objective, catalog, allowlist) {
4
- const mention = parseModelMention(objective, Object.keys(catalog));
5
- const explicit = typeof bodyModel === "string" ? matchCatalogModel(bodyModel, catalog) : undefined;
3
+ export function resolveTaskModel(bodyModel, objective, catalog, allowlist, refusals) {
4
+ const mention = parseModelMention(objective, [...Object.keys(refusals ?? {}), ...Object.keys(catalog)]);
5
+ const mentionUsable = mention.model !== undefined && Object.hasOwn(catalog, mention.model) ? mention.model : undefined;
6
+ const explicit = typeof bodyModel === "string" ? matchCatalogModel(bodyModel, catalog, refusals) : undefined;
6
7
  const present = bodyModel !== undefined && bodyModel !== null;
7
- const unknown = present && explicit === undefined;
8
8
  const repr = typeof bodyModel !== "string" ? `<non-string:${typeof bodyModel}>` : bodyModel === "" ? "<empty-string>" : bodyModel;
9
- const model = explicit ?? mention.model ?? DEFAULT_MODEL_REF;
10
- const picked = explicit !== undefined ? { ref: explicit, source: "explicit", requested: repr } : mention.model !== undefined ? { ref: mention.model, source: "mention", requested: mention.model } : undefined;
11
- const notAllowed = picked !== undefined && !isModelAllowlisted(picked.ref, catalog, allowlist)
12
- ? { requested: picked.requested, resolved: terminalCatalogName(picked.ref, catalog) ?? picked.ref, source: picked.source }
9
+ const refusedExplicit = present && explicit === undefined && typeof bodyModel === "string" ? refusedCatalogEntry(bodyModel, refusals) : undefined;
10
+ const refusedMention = explicit === undefined && refusedExplicit === undefined && mention.model !== undefined && mentionUsable === undefined ? refusedCatalogEntry(mention.model, refusals) : undefined;
11
+ const unavailable = refusedExplicit !== undefined
12
+ ? { requested: repr, name: refusedExplicit.name, refusal: refusedExplicit.refusal, source: "explicit" }
13
+ : refusedMention !== undefined
14
+ ? { requested: mention.model, name: refusedMention.name, refusal: refusedMention.refusal, source: "mention" }
15
+ : undefined;
16
+ const unknown = present && explicit === undefined && refusedExplicit === undefined;
17
+ const model = explicit ?? mentionUsable ?? DEFAULT_MODEL_REF;
18
+ const picked = explicit !== undefined ? { ref: explicit, source: "explicit", requested: repr } : mentionUsable !== undefined ? { ref: mentionUsable, source: "mention", requested: mentionUsable } : undefined;
19
+ const notAllowed = picked !== undefined && !isModelAllowlisted(picked.ref, catalog, allowlist, refusals)
20
+ ? { requested: picked.requested, resolved: terminalCatalogName(picked.ref, catalog, refusals) ?? picked.ref, source: picked.source }
13
21
  : undefined;
14
22
  return {
15
23
  model,
16
24
  cleanedObjective: mention.cleanedText || objective,
17
25
  ...(unknown ? { unknownExplicit: repr } : {}),
18
26
  ...(notAllowed ? { notAllowed } : {}),
27
+ ...(unavailable ? { unavailable } : {}),
19
28
  };
20
29
  }
21
- export function isModelAllowlisted(ref, catalog, allowlist) {
30
+ export function selectableCatalog(models, tiers, refusals) {
31
+ const expanded = (expandTiers(models, tiers ?? {}) ?? models);
32
+ const reserved = Object.keys(refusals ?? {}).filter((n) => Object.hasOwn(expanded, n) && !Object.hasOwn(models, n));
33
+ if (reserved.length === 0)
34
+ return expanded;
35
+ const out = { ...expanded };
36
+ for (const n of reserved)
37
+ delete out[n];
38
+ return out;
39
+ }
40
+ export function refusedCatalogEntry(ref, refusals) {
41
+ if (refusals === undefined)
42
+ return undefined;
43
+ if (Object.hasOwn(refusals, ref))
44
+ return { name: ref, refusal: refusals[ref] };
45
+ for (const [name, r] of Object.entries(refusals))
46
+ if (r.id === ref)
47
+ return { name, refusal: r };
48
+ return undefined;
49
+ }
50
+ export function isModelAllowlisted(ref, catalog, allowlist, refusals) {
22
51
  if (allowlist === undefined || allowlist.length === 0)
23
52
  return true;
24
- const entry = catalogEntryOf(ref, catalog);
53
+ const entry = catalogEntryOf(ref, catalog, refusals);
25
54
  if (entry === undefined)
26
55
  return true;
27
56
  for (const name of allowlist) {
28
- const allowed = catalogEntryOf(name, catalog);
57
+ const allowed = catalogEntryOf(name, catalog, refusals);
29
58
  if (allowed === undefined)
30
59
  continue;
31
60
  if (allowed === entry)
@@ -33,20 +62,22 @@ export function isModelAllowlisted(ref, catalog, allowlist) {
33
62
  }
34
63
  return false;
35
64
  }
36
- function catalogEntryOf(ref, catalog) {
37
- const name = matchCatalogModel(ref, catalog);
65
+ function catalogEntryOf(ref, catalog, refusals) {
66
+ const name = matchCatalogModel(ref, catalog, refusals);
38
67
  return name === undefined ? undefined : catalog[name];
39
68
  }
40
- export function terminalCatalogName(ref, catalog) {
41
- const key = matchCatalogModel(ref, catalog);
69
+ export function terminalCatalogName(ref, catalog, refusals) {
70
+ const key = matchCatalogModel(ref, catalog, refusals);
42
71
  if (key === undefined)
43
72
  return undefined;
44
73
  const name = catalog[key]?.name;
45
74
  return typeof name === "string" && name.length > 0 ? name : key;
46
75
  }
47
- export function matchCatalogModel(ref, catalog) {
76
+ export function matchCatalogModel(ref, catalog, refusals) {
48
77
  if (Object.hasOwn(catalog, ref))
49
78
  return ref;
79
+ if (refusals !== undefined && Object.hasOwn(refusals, ref))
80
+ return undefined;
50
81
  for (const [name, m] of Object.entries(catalog)) {
51
82
  if (name === "default")
52
83
  continue;
@@ -123,7 +123,7 @@ export declare const FAIL_OPEN_TAGS: {
123
123
  };
124
124
  readonly "server.approvals.park-row-blob-unreadable": {
125
125
  readonly cls: "F";
126
- readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` 而读)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的两位 park 行事实缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
126
+ readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
127
127
  };
128
128
  readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
129
129
  readonly cls: "F";
@@ -193,6 +193,14 @@ export declare const FAIL_OPEN_TAGS: {
193
193
  readonly cls: "F";
194
194
  readonly note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。";
195
195
  };
196
+ readonly "server.memory.locked-layer-probe-partial": {
197
+ readonly cls: "F";
198
+ readonly note: "O2(7.101.0 合并复审,`memory-layer-legacy-lock.ts announceLockedLegacyMemoryLayers`,boot 期锁住记忆层探测;detail 只报 `unreadable=<条数>`):挂载面或某个层里的目录**读不出**(EACCES 之类 —— 不是 ≤7.30.x 引擎的锁形,那种 0555 目录可读)⇒ 那棵子树看不全,照常起服。放行的最坏后果 = 一个锁住的层在 boot 日志里没被点名 —— 探测是**披露**面,不参与任何门 / 裁决;core 在真正失败的那一次删除上仍给出自己的 `chmod -R u+w` 补救句。不放行的代价严重不对称:为一条 warn 打死起服(混跑窗里能服务的副本)。计数让「这台部署的记忆树有读不出的目录」显形。";
199
+ };
200
+ readonly "server.instruction-source.announce-degraded": {
201
+ readonly cls: "F";
202
+ readonly note: "S-690(`instruction-source-notice.ts announceInstructionSource`,项目指令换源通告的唯一铸点;detail 的 `op` 点名哪一步):基线**读**失败或超期(会话店抖动 / 列形坏 / 2s 期限到,detail 带 `timeout=`)⇒ 本 run 不比也不写;基线**写**失败或超期 ⇒ 本 run 的换源帧照发(它对本 run 为真),下一 run 可能再说一遍同一次换源(超期后才落地的写由 CAS 挡住,盖不掉更新的基线);通告**出口**抛 ⇒ 本 run 缺这一帧。放行的最坏后果 = 一次换源没被说出来或被多说一次 —— 通告是**披露**面,不参与任何门 / 裁决,模型读到的指令本身不受影响。不放行的代价严重不对称:铸点跑在 core 的 `loadProjectMemory` 席内,抛回去 core 会把这次 prepare 的项目记忆整块丢掉(不注入),为一条通告赔上指令本身。计数让「某台部署的会话店一直读不出基线」与「这台部署从来没换过源」在遥测上分得开。";
203
+ };
196
204
  readonly "server.engine-notice.wire-sink-threw": {
197
205
  readonly cls: "F";
198
206
  readonly note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。";
@@ -114,7 +114,7 @@ export const FAIL_OPEN_TAGS = {
114
114
  },
115
115
  "server.approvals.park-row-blob-unreadable": {
116
116
  cls: "F",
117
- note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` 而读)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的两位 park 行事实缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
117
+ note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
118
118
  },
119
119
  "server.sql-json.checkpoint.pending-steer-queue-lossy": {
120
120
  cls: "F",
@@ -184,6 +184,14 @@ export const FAIL_OPEN_TAGS = {
184
184
  cls: "F",
185
185
  note: "#310(codex 对抗复审 R1-[medium],验真后修):三条 run 腿把一条白名单通告写进 durable 账本时,那次 `appendEvent` **异步 reject**(store 抖动 / 连接断 / 行冲突)。结构上不能 await —— `onNotice` 是 core 的同步回调,拿一次账本写去阻塞引擎是更坏的交易 —— 所以写口只能 fire-and-forget,`route` 的同步 try 结构性观察不到这次失败 —— 「投出去了」与「真落盘了」不是同一件事。放行的最坏后果按腿分档,如实登记:**sync stream 腿**还有 live SSE 那一份(在线的人看得见,丢的是断连补看);**bg / resume 两腿账本是唯一的用户可见终点** ⇒ 那条通告对用户永久消失。仍判 F 而不是 P:通告是**披露**面不是执法面(不参与任何门/裁决),且运维面那一份(结构化日志 `engine_notice`)在分流之前就已经落定、一条不丢。此前是裸 `.catch(() => undefined)` —— 同文件 resume 腿的子代 forward 写口早在 C2/C5 批2 就因为「裸吞 = 事件从 durable log 永久消失且零信号」补过 `warnAppend`,本 tag 是同一条纪律在通告口的落实(warn 留细节 + 计数让「系统性在漏」显形)。不做重试队列是刻意的:账本里没有任何一族行做有界重试,单给通告开一条会造出第二套持久化语义。",
186
186
  },
187
+ "server.memory.locked-layer-probe-partial": {
188
+ cls: "F",
189
+ note: "O2(7.101.0 合并复审,`memory-layer-legacy-lock.ts announceLockedLegacyMemoryLayers`,boot 期锁住记忆层探测;detail 只报 `unreadable=<条数>`):挂载面或某个层里的目录**读不出**(EACCES 之类 —— 不是 ≤7.30.x 引擎的锁形,那种 0555 目录可读)⇒ 那棵子树看不全,照常起服。放行的最坏后果 = 一个锁住的层在 boot 日志里没被点名 —— 探测是**披露**面,不参与任何门 / 裁决;core 在真正失败的那一次删除上仍给出自己的 `chmod -R u+w` 补救句。不放行的代价严重不对称:为一条 warn 打死起服(混跑窗里能服务的副本)。计数让「这台部署的记忆树有读不出的目录」显形。",
190
+ },
191
+ "server.instruction-source.announce-degraded": {
192
+ cls: "F",
193
+ note: "S-690(`instruction-source-notice.ts announceInstructionSource`,项目指令换源通告的唯一铸点;detail 的 `op` 点名哪一步):基线**读**失败或超期(会话店抖动 / 列形坏 / 2s 期限到,detail 带 `timeout=`)⇒ 本 run 不比也不写;基线**写**失败或超期 ⇒ 本 run 的换源帧照发(它对本 run 为真),下一 run 可能再说一遍同一次换源(超期后才落地的写由 CAS 挡住,盖不掉更新的基线);通告**出口**抛 ⇒ 本 run 缺这一帧。放行的最坏后果 = 一次换源没被说出来或被多说一次 —— 通告是**披露**面,不参与任何门 / 裁决,模型读到的指令本身不受影响。不放行的代价严重不对称:铸点跑在 core 的 `loadProjectMemory` 席内,抛回去 core 会把这次 prepare 的项目记忆整块丢掉(不注入),为一条通告赔上指令本身。计数让「某台部署的会话店一直读不出基线」与「这台部署从来没换过源」在遥测上分得开。",
194
+ },
187
195
  "server.engine-notice.wire-sink-threw": {
188
196
  cls: "F",
189
197
  note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。",
@@ -239,6 +239,7 @@ export function createMetrics() {
239
239
  m.counter("cost_quota_rejected_total", "Tasks refused because the principal is over its cumulative cost quota");
240
240
  m.counter("degraded_total", "Tasks degraded to a cheaper model (1.40), by reason (budget/rate_limit/breaker_open)");
241
241
  m.counter("task_model_not_allowlisted_resume_total", "Resumed legs whose model/compactionModel is outside the @-mention allowlist (allowed to keep running by design — the allowlist gates fresh picks only)");
242
+ m.counter("task_model_unavailable_fallback_total", "Resumed legs whose explicit model pick names a catalog entry that is unavailable on this deployment (no brain mounted for its provider / entry fails its schema) — the leg runs on the default model; fresh submits get 400 request.model_unavailable");
242
243
  m.counter("prompt_constitution_total", "Prompt assembly constitution mode per task (core 1.243 提示词主权): steady state = core; provider-assembled/replaced/legacy warrant eyes");
243
244
  m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the disclosure text carried no readable code). COARSE on purpose — whether a failure is permanent (the same file will fail every turn until the deployment changes) is counted by file_history_permanent_failure_total instead, from the typed error object rather than from prose");
244
245
  m.counter("verifications_total", "Tasks run through the verification gate (1.44), by final verdict");