@sema-agent/server 7.99.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 (174) hide show
  1. package/MIGRATION.md +35 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +150 -56
  5. package/dist/approval-ask-machine.d.ts +10 -0
  6. package/dist/approval-ask-machine.js +3 -0
  7. package/dist/approval-card.d.ts +86 -1
  8. package/dist/approval-card.js +42 -8
  9. package/dist/approval-policy-settlement.d.ts +31 -0
  10. package/dist/approval-policy-settlement.js +16 -0
  11. package/dist/approval-reconciler.d.ts +8 -1
  12. package/dist/approval-reconciler.js +5 -4
  13. package/dist/approval.d.ts +44 -20
  14. package/dist/approval.js +14 -6
  15. package/dist/boot/config-center.d.ts +16 -8
  16. package/dist/boot/config-center.js +114 -90
  17. package/dist/boot/execution-env.d.ts +1 -2
  18. package/dist/boot/execution-env.js +21 -3
  19. package/dist/boot/leader.js +12 -9
  20. package/dist/boot/memory-consolidation.d.ts +38 -0
  21. package/dist/boot/memory-consolidation.js +35 -0
  22. package/dist/boot/parked-revive-gate.js +1 -0
  23. package/dist/boot/reapers.js +1 -0
  24. package/dist/boot/resolve-spec.d.ts +0 -1
  25. package/dist/boot/resolve-spec.js +28 -10
  26. package/dist/boot/runner-deps.d.ts +2 -2
  27. package/dist/boot/runner-deps.js +9 -3
  28. package/dist/boot/stage-02-registry-pull.js +9 -0
  29. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +3 -3
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  33. package/dist/boot/stage-07-capability-layer.js +6 -6
  34. package/dist/boot/stage-08-reapers.d.ts +3 -4
  35. package/dist/boot/stage-08-reapers.js +2 -2
  36. package/dist/boot/stage-09-leader.d.ts +2 -3
  37. package/dist/boot/stage-10-http-server.d.ts +2 -3
  38. package/dist/boot/stage-10-http-server.js +2 -2
  39. package/dist/boot/stores.js +4 -0
  40. package/dist/capabilities/center-plugins.d.ts +14 -2
  41. package/dist/capabilities/center-plugins.js +13 -5
  42. package/dist/capabilities/hands-lane.js +2 -1
  43. package/dist/capabilities/scenarios.js +18 -5
  44. package/dist/capabilities/tool-defer.d.ts +2 -2
  45. package/dist/config-catalog.d.ts +2 -2
  46. package/dist/config-catalog.js +16 -15
  47. package/dist/config-center/apply-effective.d.ts +59 -39
  48. package/dist/config-center/apply-effective.js +231 -120
  49. package/dist/config-center/apply-ledger.d.ts +45 -16
  50. package/dist/config-center/apply-ledger.js +28 -18
  51. package/dist/config-center/facade.d.ts +12 -6
  52. package/dist/config-center/facade.js +2 -2
  53. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  54. package/dist/config-center/hot-keys-registry.js +11 -7
  55. package/dist/config-center/http-client.d.ts +4 -1
  56. package/dist/config-center/http-client.js +15 -10
  57. package/dist/config-center/read-warnings.d.ts +115 -6
  58. package/dist/config-center/read-warnings.js +115 -5
  59. package/dist/config-center/restart-signal.d.ts +21 -14
  60. package/dist/config-center/restart-signal.js +16 -22
  61. package/dist/config-center/types.d.ts +9 -3
  62. package/dist/config-invariants.d.ts +2 -2
  63. package/dist/config-invariants.js +15 -0
  64. package/dist/config-provider.d.ts +4 -0
  65. package/dist/config-provider.js +64 -9
  66. package/dist/config-types.d.ts +57 -13
  67. package/dist/config.js +87 -22
  68. package/dist/cross-session-settings.js +1 -1
  69. package/dist/deployment-governance.d.ts +45 -12
  70. package/dist/deployment-governance.js +37 -9
  71. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  72. package/dist/env-name-allowlist-knobs.js +24 -0
  73. package/dist/fleet-client.d.ts +2 -1
  74. package/dist/hooks/hook-llm.js +13 -4
  75. package/dist/host-lsp-manager.d.ts +41 -0
  76. package/dist/host-lsp-manager.js +15 -0
  77. package/dist/http/active-run-conflict.d.ts +2 -12
  78. package/dist/http/active-run-conflict.js +1 -6
  79. package/dist/http/admission.js +30 -7
  80. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  81. package/dist/http/memory-store-refusal-reply.js +10 -0
  82. package/dist/http/resume-legs.d.ts +1 -1
  83. package/dist/http/resume-legs.js +14 -10
  84. package/dist/http/route-ctx.d.ts +2 -2
  85. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  86. package/dist/http/routes/approvals-assistant.js +3 -2
  87. package/dist/http/routes/capabilities.js +3 -3
  88. package/dist/http/routes/memory-bundle.js +3 -0
  89. package/dist/http/routes/memory-compliance.js +3 -0
  90. package/dist/http/routes/memory-origin.js +3 -0
  91. package/dist/http/routes/sessions.js +7 -0
  92. package/dist/http/routes/side-query.js +10 -4
  93. package/dist/http/routes/tasks.js +7 -6
  94. package/dist/http/server.d.ts +6 -5
  95. package/dist/http/wire-types.d.ts +6 -3
  96. package/dist/instruction-source-notice.d.ts +97 -0
  97. package/dist/instruction-source-notice.js +78 -0
  98. package/dist/leader/wire.d.ts +20 -12
  99. package/dist/leader/wire.js +7 -6
  100. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  101. package/dist/memory-layer-legacy-lock.js +74 -0
  102. package/dist/memory-store-refusal.d.ts +37 -0
  103. package/dist/memory-store-refusal.js +14 -0
  104. package/dist/model-entry-refusal.d.ts +130 -0
  105. package/dist/model-entry-refusal.js +157 -0
  106. package/dist/model-provider.d.ts +9 -3
  107. package/dist/model-route-endpoint.d.ts +1 -1
  108. package/dist/model-select.d.ts +49 -4
  109. package/dist/model-select.js +48 -17
  110. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  111. package/dist/observability/corrupt-read-seat.js +31 -0
  112. package/dist/observability/fail-open.d.ts +16 -4
  113. package/dist/observability/fail-open.js +16 -4
  114. package/dist/observability/metrics.js +1 -0
  115. package/dist/observability/run-terminal.js +1 -0
  116. package/dist/observability/secret-env-scrub.d.ts +27 -3
  117. package/dist/observability/secret-env-scrub.js +22 -6
  118. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  119. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  120. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  121. package/dist/plugins/approval-ask-store-file.js +21 -3
  122. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  123. package/dist/plugins/approval-ask-store-memory.js +39 -13
  124. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  125. package/dist/plugins/approval-ask-store-sql.js +27 -3
  126. package/dist/plugins/caching-session-store.d.ts +6 -0
  127. package/dist/plugins/caching-session-store.js +6 -0
  128. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  129. package/dist/plugins/checkpoint-store-sql.js +19 -2
  130. package/dist/plugins/local-checkpoint-store.js +2 -0
  131. package/dist/plugins/local-session-store.d.ts +20 -25
  132. package/dist/plugins/local-session-store.js +111 -64
  133. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  134. package/dist/plugins/mailbox-store-sql.js +6 -4
  135. package/dist/plugins/permission-rule-store-file.js +3 -2
  136. package/dist/plugins/pg-session-storage.d.ts +7 -0
  137. package/dist/plugins/pg-session-storage.js +19 -0
  138. package/dist/plugins/remote-env-host.d.ts +3 -1
  139. package/dist/plugins/remote-env-host.js +3 -6
  140. package/dist/plugins/sql-json-column.d.ts +16 -3
  141. package/dist/plugins/sql-json-column.js +15 -4
  142. package/dist/plugins/tidb-pool.js +5 -0
  143. package/dist/plugins/tidb-session-store.d.ts +13 -0
  144. package/dist/plugins/tidb-session-store.js +14 -0
  145. package/dist/plugins/web-search.d.ts +44 -23
  146. package/dist/plugins/web-search.js +161 -50
  147. package/dist/project-memory.d.ts +10 -0
  148. package/dist/project-memory.js +28 -14
  149. package/dist/run-local.d.ts +17 -15
  150. package/dist/run-local.js +119 -18
  151. package/dist/runs.js +6 -5
  152. package/dist/runtime-governance.d.ts +50 -3
  153. package/dist/runtime-governance.js +5 -0
  154. package/dist/sealed-key.d.ts +5 -2
  155. package/dist/server-secret-env.d.ts +26 -2
  156. package/dist/server-secret-env.js +9 -0
  157. package/dist/task-settings.d.ts +1 -1
  158. package/dist/tool-approval.d.ts +150 -28
  159. package/dist/tool-approval.js +150 -54
  160. package/dist/trace/core-keyset-guard.d.ts +30 -8
  161. package/dist/trace/engine-notice-wire.d.ts +27 -1
  162. package/dist/trace/engine-notice-wire.js +8 -0
  163. package/dist/trace/ledger-sink.js +2 -2
  164. package/dist/trace/project.d.ts +13 -0
  165. package/dist/trace/project.js +22 -5
  166. package/dist/trace/projection-drop.d.ts +2 -0
  167. package/dist/trace/projection-drop.js +6 -0
  168. package/dist/trace/sema-provenance.d.ts +4 -1
  169. package/dist/trace/sema-provenance.js +1 -1
  170. package/dist/trace/task-notification-facets.d.ts +65 -0
  171. package/dist/trace/task-notification-facets.js +31 -0
  172. package/dist/trace/wire-projection-faces.d.ts +3 -3
  173. package/dist/trace/wire-projection-faces.js +3 -3
  174. package/package.json +3 -3
@@ -1,13 +1,16 @@
1
- import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/settings-schema";
1
+ import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
2
  import { sealedKeyPoison } from "../sealed-key.js";
3
3
  import { applyAutoCompactWindow, buildModelCostKey, findUnmatchableToolNames, formatUnmatchableToolNames, parseAutonomy } from "../config.js";
4
4
  import { buildModelCompat, readModelCompat } from "../model-compat.js";
5
5
  import { validateCommandRules } from "../runtime-governance.js";
6
6
  import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabilities/collab-workflows.js";
7
7
  import { stageLimits } from "./stage-limits.js";
8
- import { BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
8
+ import { APPROVAL_GROUP_KEYS, BATCH1_LIMIT_KEYS } from "./hot-keys-registry.js";
9
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
10
10
  import { crossInvariant } from "../config-invariants.js";
11
+ import { judgeCatalogEntries, mintedRefusedNames, refusalEventOf, refusalWarnFields, REFUSED_NAME_MINTED_CODE, unreadableDroppedRefusal } from "../model-entry-refusal.js";
12
+ import { gateIntentUnserviceableReason } from "../approval.js";
13
+ import { refusedGroups } from "./apply-ledger.js";
11
14
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
12
15
  function envDerivedModel(config) {
13
16
  const m = config.envModel;
@@ -93,7 +96,7 @@ function stageAtModelAllowlist(raw, models, logger) {
93
96
  }
94
97
  return names;
95
98
  }
96
- export function applyEffective(config, eff, logger, opts = {}) {
99
+ export function applyEffective(config, eff, logger, opts) {
97
100
  const staged = stageEffective(config, eff, logger, opts);
98
101
  const gen = opts.generation ?? staged.version;
99
102
  const live = appliedGeneration.get(config);
@@ -102,9 +105,10 @@ export function applyEffective(config, eff, logger, opts = {}) {
102
105
  logger?.warn("sema_registry_stale_generation_refused", { staged: staged.version, stagedOrdinal: gen, live, note: "an older staged generation must not overwrite a newer committed one — refused whole; next poll replays" });
103
106
  return false;
104
107
  }
105
- if (staged.modelPlane !== undefined && opts.swapPlane !== undefined) {
108
+ const mp = staged.modelPlaneGroup.stages.models;
109
+ if (mp !== undefined && opts.swapPlane !== undefined) {
106
110
  try {
107
- opts.swapPlane({ models: staged.modelPlane.models, tiers: staged.modelPlane.activeTiers });
111
+ opts.swapPlane({ models: mp.models, tiers: mp.activeTiers });
108
112
  }
109
113
  catch (err) {
110
114
  logger?.warn("sema_registry_model_plane_swap_refused", {
@@ -118,25 +122,31 @@ export function applyEffective(config, eff, logger, opts = {}) {
118
122
  commitStaged(config, staged);
119
123
  if (ordered)
120
124
  appliedGeneration.set(config, gen);
121
- opts.report?.({
122
- version: staged.version,
123
- ...(opts.generation !== undefined ? { ordinal: opts.generation } : {}),
124
- limits: {
125
- applied: staged.limits.changed,
126
- ...(staged.limits.deferred.length > 0 ? { deferred: staged.limits.deferred } : {}),
127
- ...(staged.limits.rejected ? { rejected: staged.limits.rejected } : {}),
128
- },
129
- fullyApplied: staged.limits.rejected === undefined,
130
- });
125
+ if (opts.report) {
126
+ const report = {
127
+ version: staged.version,
128
+ ...(opts.generation !== undefined ? { ordinal: opts.generation } : {}),
129
+ groups: {
130
+ limitsHttp: {
131
+ applied: staged.limits.changed,
132
+ ...(staged.limits.deferred.length > 0 ? { deferred: staged.limits.deferred } : {}),
133
+ ...(staged.limits.rejected ? { rejected: staged.limits.rejected } : {}),
134
+ },
135
+ approvalGate: groupReportOf(staged.approvalGate),
136
+ modelPlane: modelPlaneGroupReport(staged.modelPlaneGroup),
137
+ },
138
+ };
139
+ opts.report({ ...report, fullyApplied: refusedGroups(report).length === 0 });
140
+ }
131
141
  if (staged.limits.changed.length > 0)
132
142
  logger?.info("sema_registry_limits", { version: staged.version, keys: staged.limits.changed });
133
- if (staged.modelPlane)
134
- logger?.info("sema_registry_models", staged.modelPlane.infoLine);
135
- if (staged.gatesInfo)
136
- logger?.info("sema_registry_runtime", staged.gatesInfo);
143
+ if (mp)
144
+ logger?.info("sema_registry_models", mp.infoLine);
145
+ if (staged.approvalGate.next !== undefined)
146
+ logger?.info("sema_registry_runtime", { version: staged.version, approvalRequire: staged.approvalGate.next });
137
147
  if (Object.keys(staged.hot.infoLine).length > 0)
138
148
  logger?.info("sema_registry_runtime_hot", staged.hot.infoLine);
139
- for (const n of staged.collabPlane?.notices ?? []) {
149
+ for (const n of staged.modelPlaneGroup.stages.collab?.notices ?? []) {
140
150
  if (n.level === "warn")
141
151
  logger?.warn(n.msg, n.fields);
142
152
  else
@@ -147,10 +157,11 @@ export function applyEffective(config, eff, logger, opts = {}) {
147
157
  return true;
148
158
  }
149
159
  function commitStaged(config, staged) {
150
- const mp = staged.modelPlane;
160
+ const mp = staged.modelPlaneGroup.stages.models;
151
161
  if (mp) {
152
162
  config.modelApiKeyEnv = mp.modelApiKeyEnv;
153
163
  config.modelApiKeys = mp.modelApiKeys;
164
+ config.modelRefusals = mp.modelRefusals;
154
165
  mutateInPlace(config.modelQuotaWeights, mp.quotaWeights);
155
166
  mutateInPlace(config.models, mp.models);
156
167
  config.model = config.models.default;
@@ -158,10 +169,8 @@ function commitStaged(config, staged) {
158
169
  mutateInPlace(config.tiers, mp.activeTiers);
159
170
  config.atModelAllowlist = mp.atModelAllowlist;
160
171
  }
161
- if (staged.gateAssignments) {
162
- for (const [k, v] of staged.gateAssignments)
163
- config[k] = v;
164
- }
172
+ if (staged.approvalGate.next !== undefined)
173
+ config.approvalRequire = staged.approvalGate.next;
165
174
  for (const [k, v] of staged.limits.assignments)
166
175
  config[k] = v;
167
176
  for (const k of staged.limits.deletions)
@@ -170,62 +179,90 @@ function commitStaged(config, staged) {
170
179
  config.autonomy = staged.hot.autonomyNext;
171
180
  if (staged.hot.setCommandPolicy)
172
181
  config.commandPolicy = staged.hot.commandPolicyNext;
173
- if (staged.collabPlane)
174
- registerCollabWorkflows(staged.collabPlane.workflows);
182
+ const collab = staged.modelPlaneGroup.stages.collab;
183
+ if (collab !== undefined)
184
+ registerCollabWorkflows(collab.workflows);
175
185
  mutateInPlace(config.projects, staged.projects);
176
186
  }
177
- function stageEffective(config, eff, logger, opts) {
178
- if (!opts.teamsOnly && !eff.version) {
179
- logger?.warn("sema_registry_unpublished", {
180
- version: eff.version,
181
- note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
182
- });
183
- }
187
+ export const MODEL_PLANE_DOMAINS = ["models", "collab"];
188
+ const MODEL_PLANE_SURFACES = {
189
+ models: ["models", "model", "modelRefusals", "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "roles", "tiers", "atModelAllowlist"],
190
+ collab: ["collabWorkflows"],
191
+ };
192
+ const MODEL_PLANE_GROUP_KEYS = MODEL_PLANE_DOMAINS.flatMap((d) => MODEL_PLANE_SURFACES[d]);
193
+ export function withModelPlaneOf(target, source) {
194
+ const out = { ...target };
195
+ for (const d of MODEL_PLANE_DOMAINS)
196
+ takeModelPlaneDomain(out, source, d);
197
+ return out;
198
+ }
199
+ function takeModelPlaneDomain(out, source, d) {
200
+ const v = source[d];
201
+ if (v === undefined)
202
+ delete out[d];
203
+ else
204
+ out[d] = v;
205
+ }
206
+ export function buildEnvOnlyModelPlane() {
207
+ return { models: { models: [], roles: {} } };
208
+ }
209
+ function modelPlaneGroupReport(group) {
210
+ return {
211
+ applied: MODEL_PLANE_DOMAINS.flatMap((d) => (group.stages[d] !== undefined ? MODEL_PLANE_SURFACES[d] : [])),
212
+ ...(group.rejected !== undefined ? { rejected: { keys: group.rejected.keys, reason: group.rejected.reason } } : {}),
213
+ };
214
+ }
215
+ const ROLE_BINDING_UNAVAILABLE_HINT = "tasks resolving this role fail: alias/tier expansion cannot mint this name; the engine refuses it loud — bind the role to a usable model, or make the entry usable (see the entry's own warn)";
216
+ function stageModelPlane(config, eff, logger, sealedKeys) {
184
217
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
185
- let modelPlane;
186
- if (enabled.length > 0 && opts.deferModelPlane !== true) {
218
+ const droppedEnabled = eff.models?.droppedIdentities;
219
+ if (enabled.length === 0 && !(Array.isArray(droppedEnabled) && droppedEnabled.length > 0))
220
+ return { stages: { collab: stageCollabDomain(eff) } };
221
+ {
222
+ const verdict = judgeCatalogEntries(enabled, { anthropicRouteMounted: config.anthropic !== undefined }, eff.models.droppedIdentities);
223
+ for (const [name, r] of Object.entries(verdict.refused))
224
+ logger?.warn(refusalEventOf(r.reason), refusalWarnFields(name, r));
225
+ const refuse = (invariant, reason, detail = {}) => {
226
+ logger?.warn("sema_registry_model_plane_refused", {
227
+ version: typeof eff.version === "number" ? eff.version : 0,
228
+ invariant,
229
+ reason,
230
+ ...detail,
231
+ kept: "the model plane of this candidate is refused as a group — the current model plane (catalog / roles / tiers / refusals / allowlist, and the collab workflows that reference it) keeps serving and the other groups of this candidate still apply; the candidate is not fully applied (LKG / ETag stay on the last fully applied generation) and is re-judged next pull",
232
+ });
233
+ return { stages: {}, rejected: { keys: MODEL_PLANE_GROUP_KEYS, reason, invariant }, refused: verdict.refused };
234
+ };
235
+ if (verdict.unreadable.length > 0) {
236
+ logger?.warn("sema_registry_model_entry_dropped_unproven", {
237
+ entries: [...verdict.unreadable],
238
+ code: REFUSED_NAME_MINTED_CODE,
239
+ hint: "these catalog entries were dropped by the settings-schema read and the raw document could not be matched to the snapshot that was parsed (e.g. the local file changed between two reads) — their names cannot be proven, so no tombstone can be minted and the candidate's model plane is refused (a name nobody can prove cannot be shown not to be re-minted by the tier/alias expansion); the next read re-judges",
240
+ });
241
+ return refuse("refused-names-not-minted", unreadableDroppedRefusal(verdict.unreadable));
242
+ }
243
+ const usable = verdict.usable;
187
244
  const models = {};
188
245
  const modelApiKeyEnv = {};
189
246
  const modelApiKeys = {};
190
247
  const envModel = envDerivedModel(config);
191
248
  const envDefaults = { maxTokens: envModel.maxTokens, contextWindow: envModel.contextWindow, reasoning: envModel.reasoning, charsPerToken: envModel.charsPerToken, defaultThinking: envModel.defaultThinking, compat: envModel.compat };
192
- for (const m of enabled) {
193
- const rawBaseUrl = m.baseUrl;
194
- let safeUrl = rawBaseUrl;
195
- if (rawBaseUrl) {
196
- let ok = false;
197
- try {
198
- const u = new URL(rawBaseUrl);
199
- ok = u.protocol === "http:" || u.protocol === "https:";
200
- }
201
- catch {
202
- ok = false;
203
- }
204
- if (ok)
205
- logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
206
- else {
207
- logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the deployment gateway (MODEL_GATEWAY_BASEURL); with none declared the whole candidate is refused (model-route-endpoint)" });
208
- safeUrl = undefined;
209
- }
210
- }
211
- models[m.name] = toModel(safeUrl === rawBaseUrl ? m : { ...m, baseUrl: safeUrl }, envDefaults, logger);
249
+ for (const m of usable) {
250
+ if (m.baseUrl)
251
+ logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(m.baseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
252
+ models[m.name] = toModel(m, envDefaults, logger);
212
253
  if (m.sealedApiKey) {
213
254
  const s = m.sealedApiKey;
214
- if (s.alg !== SEALED_BOX_ALG) {
215
- logger?.warn("sema_registry_model_sealed_key_bad_alg", { model: m.name, alg: String(s.alg), expected: SEALED_BOX_ALG, hint: "unsupported sealed-box alg — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback; contract v1 pins the alg)" });
216
- modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "unsupported_alg");
217
- }
218
- else if (!opts.sealedKeys) {
255
+ if (!sealedKeys) {
219
256
  logger?.warn("sema_registry_model_sealed_key_no_store", { model: m.name, publicKeyId: s.publicKeyId, hint: "no sealed-key store on this host (boot custody init failed or lane unwired) — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
220
257
  modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "no_key_store");
221
258
  }
222
- else if (!opts.sealedKeys.has(s.publicKeyId)) {
223
- logger?.warn("sema_registry_model_sealed_key_no_private_key", { model: m.name, publicKeyId: s.publicKeyId, held: opts.sealedKeys.ids(), hint: "ciphertext targets a private key this host doesn't hold (rotated away / other host) — key POISONED: tasks on this model fail loud until the operator re-pastes the key in the web UI (it seals against the newest registered public key)" });
259
+ else if (!sealedKeys.has(s.publicKeyId)) {
260
+ logger?.warn("sema_registry_model_sealed_key_no_private_key", { model: m.name, publicKeyId: s.publicKeyId, held: sealedKeys.ids(), hint: "ciphertext targets a private key this host doesn't hold (rotated away / other host) — key POISONED: tasks on this model fail loud until the operator re-pastes the key in the web UI (it seals against the newest registered public key)" });
224
261
  modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "stale_ciphertext");
225
262
  }
226
263
  else {
227
264
  try {
228
- modelApiKeys[m.name] = opts.sealedKeys.open(s.ciphertext, s.publicKeyId);
265
+ modelApiKeys[m.name] = sealedKeys.open(s.ciphertext, s.publicKeyId);
229
266
  }
230
267
  catch (err) {
231
268
  logger?.warn("sema_registry_model_sealed_key_open_failed", { model: m.name, publicKeyId: s.publicKeyId, err: String(err), hint: "corrupt/foreign ciphertext — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
@@ -239,12 +276,9 @@ function stageEffective(config, eff, logger, opts) {
239
276
  else
240
277
  logger?.warn("sema_registry_model_key_env_missing", { model: m.name, apiKeyEnv: m.apiKeyEnv, hint: "env var not set in this service — model falls back to the gateway key" });
241
278
  }
242
- if (m.provider === "anthropic" && !config.anthropic) {
243
- logger?.warn("sema_registry_model_no_brain", { model: m.name, provider: m.provider, hint: "set ANTHROPIC_API_KEY in this service's env, else it mis-routes to the gateway brain" });
244
- }
245
279
  }
246
280
  const quotaWeights = {};
247
- for (const m of enabled) {
281
+ for (const m of usable) {
248
282
  const qw = typeof m.quotaWeight === "number" && Number.isFinite(m.quotaWeight) && m.quotaWeight > 0 ? m.quotaWeight : undefined;
249
283
  if (qw !== undefined) {
250
284
  quotaWeights[m.name] = qw;
@@ -262,41 +296,80 @@ function stageEffective(config, eff, logger, opts) {
262
296
  centerRoles[role] = { select: tgt.select };
263
297
  }
264
298
  const roles = { ...envRolesBaseline.get(config), ...centerRoles };
265
- const activeTiers = resolveActiveTiers(eff.models) ?? {};
266
- const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
267
- models.default = models[picked.name];
268
- const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: { ...config, models } });
269
- if (endpointRefusal !== undefined) {
270
- throw new Error(`candidate model plane refused whole (cross-invariant model-route-endpoint): ${endpointRefusal} ` +
271
- "The current generation keeps serving; the candidate is re-judged on the next pull.");
299
+ let picked;
300
+ if (usable.length > 0) {
301
+ picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, usable[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", {
302
+ source,
303
+ name,
304
+ ...(Object.hasOwn(verdict.refused, name) ? { unavailable: verdict.refused[name].code } : {}),
305
+ hint: "explicit default names a model that is not in the usable catalog (not enabled, or unavailable on this host — see `unavailable`) — falling to the next source",
306
+ }));
307
+ models.default = models[picked.name];
308
+ }
309
+ const activeTiers = {};
310
+ for (const [tier, target] of Object.entries(resolveActiveTiers(eff.models) ?? {})) {
311
+ if (models[target] !== undefined) {
312
+ activeTiers[tier] = target;
313
+ continue;
314
+ }
315
+ const refusal = Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
316
+ logger?.warn("sema_registry_tier_binding_unavailable", { tier, model: target, ...(refusal !== undefined ? { code: refusal.code } : {}), hint: "this tier binding names a model that is not in this generation's usable catalog — the binding is dropped (the tier word falls to the next lower bound tier, core's ladder); fix the entry or rebind the tier" });
317
+ }
318
+ const nextView = { ...config, models, tiers: activeTiers, modelRefusals: verdict.refused };
319
+ const noDefault = crossInvariant("default-model-resolvable").check({ live: config, next: nextView });
320
+ if (noDefault !== undefined)
321
+ return refuse("default-model-resolvable", noDefault);
322
+ const endpointRefusal = crossInvariant("model-route-endpoint").check({ live: config, next: nextView });
323
+ if (endpointRefusal !== undefined)
324
+ return refuse("model-route-endpoint", endpointRefusal);
325
+ const minted = crossInvariant("refused-names-not-minted").check({ live: config, next: nextView });
326
+ if (minted !== undefined)
327
+ return refuse("refused-names-not-minted", minted, { minted: mintedRefusedNames(nextView) });
328
+ for (const [role, spec] of Object.entries(roles)) {
329
+ const target = typeof spec === "string" ? spec : spec !== null && typeof spec === "object" && "model" in spec && typeof spec.model === "string" ? spec.model : undefined;
330
+ const refusal = target !== undefined && Object.hasOwn(verdict.refused, target) ? verdict.refused[target] : undefined;
331
+ if (refusal !== undefined) {
332
+ logger?.warn("sema_registry_role_binding_unavailable", { role, model: target, code: refusal.code, hint: ROLE_BINDING_UNAVAILABLE_HINT });
333
+ }
272
334
  }
273
335
  const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
274
- modelPlane = {
336
+ const defaultModel = models.default;
337
+ const plane = {
275
338
  models,
276
339
  modelApiKeyEnv,
277
340
  modelApiKeys,
341
+ modelRefusals: { ...verdict.refused },
278
342
  quotaWeights,
279
343
  roles,
280
344
  activeTiers,
281
345
  atModelAllowlist,
282
- infoLine: { count: enabled.length, default: models.default.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
346
+ infoLine: { count: usable.length, ...(Object.keys(verdict.refused).length > 0 ? { unavailable: Object.keys(verdict.refused) } : {}), default: defaultModel.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
283
347
  };
348
+ return { stages: { models: plane, collab: stageCollabDomain(eff) }, defaultName: picked.name, refused: verdict.refused };
349
+ }
350
+ }
351
+ function stageCollabDomain(eff) {
352
+ const projected = projectCollabToWorkflows(eff.collab?.templates);
353
+ const notices = [];
354
+ if (Object.keys(projected.workflows).length > 0)
355
+ notices.push({ level: "info", msg: "sema_registry_collab_workflows", fields: { workflows: Object.keys(projected.workflows) } });
356
+ if (projected.skipped.length > 0)
357
+ notices.push({ level: "warn", msg: "sema_registry_collab_skipped", fields: { skipped: projected.skipped } });
358
+ if (projected.notes.length > 0)
359
+ notices.push({ level: "info", msg: "sema_registry_collab_notes", fields: { notes: projected.notes } });
360
+ return { workflows: projected.workflows, notices };
361
+ }
362
+ function stageEffective(config, eff, logger, opts) {
363
+ if (opts.leg === "boot" && !eff.version) {
364
+ logger?.warn("sema_registry_unpublished", {
365
+ version: eff.version,
366
+ note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
367
+ });
284
368
  }
285
- const gates = opts.teamsOnly ? undefined : stageApprovalGate(eff.governance, logger);
369
+ const modelPlaneGroup = stageModelPlane(config, eff, logger, opts.sealedKeys);
370
+ const approvalGate = stageApprovalGate(config, eff.governance, opts, logger);
286
371
  const limits = stageLimits(config, { version: typeof eff.version === "number" ? eff.version : 0, ...("limits" in eff ? { limits: eff.limits } : {}), legacyGates: legacyGateValues(eff.runtime) }, logger);
287
372
  const hot = stageRuntimeHot(config, eff.governance, logger);
288
- let collabPlane;
289
- if (opts.deferModelPlane !== true) {
290
- const projected = projectCollabToWorkflows(eff.collab?.templates);
291
- const notices = [];
292
- if (Object.keys(projected.workflows).length > 0)
293
- notices.push({ level: "info", msg: "sema_registry_collab_workflows", fields: { workflows: Object.keys(projected.workflows) } });
294
- if (projected.skipped.length > 0)
295
- notices.push({ level: "warn", msg: "sema_registry_collab_skipped", fields: { skipped: projected.skipped } });
296
- if (projected.notes.length > 0)
297
- notices.push({ level: "info", msg: "sema_registry_collab_notes", fields: { notes: projected.notes } });
298
- collabPlane = { workflows: projected.workflows, notices };
299
- }
300
373
  const projDomain = eff.projects;
301
374
  const rawProjects = (projDomain && typeof projDomain === "object" && !Array.isArray(projDomain) &&
302
375
  "projects" in projDomain && typeof projDomain.projects === "object"
@@ -315,16 +388,14 @@ function stageEffective(config, eff, logger, opts) {
315
388
  }
316
389
  return {
317
390
  version: typeof eff.version === "number" && Number.isFinite(eff.version) ? eff.version : 0,
318
- ...(modelPlane !== undefined ? { modelPlane } : {}),
319
- ...(gates !== undefined ? { gateAssignments: gates.assignments, ...(gates.info !== undefined ? { gatesInfo: gates.info } : {}) } : {}),
391
+ modelPlaneGroup,
392
+ approvalGate,
320
393
  limits,
321
394
  hot,
322
- ...(collabPlane !== undefined ? { collabPlane } : {}),
323
395
  projects,
324
396
  };
325
397
  }
326
398
  export const RUNTIME_GATE_KEYS = ["rateLimitPerMin", "maxTaskCostUsd", "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec"];
327
- export const BOOT_GATE_KEYS = ["approvalRequire"];
328
399
  export function centerManagedConfigKeys(eff) {
329
400
  const out = new Set();
330
401
  if (!eff)
@@ -372,40 +443,76 @@ export function runtimeGatePresent(rt, key) {
372
443
  return typeof v === "number" && v > 0;
373
444
  return typeof v === "number";
374
445
  }
375
- function stageApprovalRequire(raw, logger) {
376
- const refuse = (detail, names, kept) => {
377
- logger?.warn("sema_registry_approval_require_invalid", { detail, names: [...names], kept });
378
- return {};
379
- };
380
- const KEPT_WOULD_MATCH_NOTHING = "the prior approvalRequire is unchanged — the published list would have silently matched nothing";
381
- const KEPT_WOULD_REMOVE_GATE = "the prior approvalRequire is unchanged — coercing this malformed value would have silently REMOVED the approval gate";
446
+ const KEPT_WOULD_MATCH_NOTHING = "the prior approvalRequire is unchanged — the published list would have silently matched nothing";
447
+ const KEPT_WOULD_REMOVE_GATE = "the prior approvalRequire is unchanged — coercing this malformed value would have silently REMOVED the approval gate";
448
+ const KEPT_WOULD_NEVER_PARK = "the prior approvalRequire is unchanged — this deployment has nowhere to park a gated call, so applying the list would leave it silently unenforced";
449
+ function stageApprovalRequire(raw) {
382
450
  if (!Array.isArray(raw)) {
383
- return refuse(`published runtime.approvalRequire must be an ARRAY of whole tool names — got ${raw === null ? "null" : typeof raw}. ` +
384
- `Refusing the domain instead of coercing it: an empty list means "no approval gate", so coercing a malformed value would SILENTLY remove the gate.`, [], KEPT_WOULD_REMOVE_GATE);
451
+ const detail = `published governance.approvalRequire must be an ARRAY of whole tool names — got ${raw === null ? "null" : typeof raw}. ` +
452
+ `Refusing the group instead of coercing it: an empty list means "no approval gate", so coercing a malformed value would SILENTLY remove the gate.`;
453
+ return { ok: false, detail, reason: detail, names: [], kept: KEPT_WOULD_REMOVE_GATE };
385
454
  }
386
455
  const nonString = raw
387
456
  .map((n, i) => (typeof n === "string" ? undefined : { i, t: n === null ? "null" : typeof n }))
388
457
  .filter((e) => e !== undefined);
389
458
  if (nonString.length > 0) {
390
- return refuse(`published runtime.approvalRequire contains ${nonString.length} non-string entr${nonString.length === 1 ? "y" : "ies"} ` +
459
+ const detail = `published governance.approvalRequire contains ${nonString.length} non-string entr${nonString.length === 1 ? "y" : "ies"} ` +
391
460
  `(${nonString.map((e) => `[${e.i}]:${e.t}`).join(", ")}) — every entry must be a whole tool name string. ` +
392
- `Refusing the domain instead of dropping them: dropping every entry would SILENTLY remove the approval gate.`, nonString.map((e) => `[${e.i}]`), KEPT_WOULD_REMOVE_GATE);
461
+ `Refusing the group instead of dropping them: dropping every entry would SILENTLY remove the approval gate.`;
462
+ return { ok: false, detail, reason: detail, names: nonString.map((e) => `[${e.i}]`), kept: KEPT_WOULD_REMOVE_GATE };
393
463
  }
394
464
  const names = raw.filter((n) => typeof n === "string").map((n) => n.trim()).filter((n) => n.length > 0);
395
465
  const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
396
466
  if (bad.length > 0) {
397
- return refuse(formatUnmatchableToolNames("published runtime.approvalRequire", bad), bad.map((b) => b.name), KEPT_WOULD_MATCH_NOTHING);
467
+ return {
468
+ ok: false,
469
+ detail: formatUnmatchableToolNames("published governance.approvalRequire", bad),
470
+ reason: `published governance.approvalRequire has ${bad.length} entr${bad.length === 1 ? "y" : "ies"} that match no live tool (retired name / incomplete MCP name / CC-form rule) — the offending entries are named in the sema_registry_approval_require_invalid log line`,
471
+ names: bad.map((b) => b.name),
472
+ kept: KEPT_WOULD_MATCH_NOTHING,
473
+ };
398
474
  }
399
- return { value: names };
475
+ return { ok: true, value: names };
400
476
  }
401
- function stageApprovalGate(gov, logger) {
477
+ function unserviceableOnLeg(leg, candidate) {
478
+ switch (leg.leg) {
479
+ case "boot":
480
+ return undefined;
481
+ case "hot":
482
+ return gateIntentUnserviceableReason(candidate, leg.checkpointStorePresent);
483
+ default: {
484
+ const unknownLeg = leg;
485
+ throw new Error(`applyEffective: unknown apply leg ${JSON.stringify(unknownLeg)}`);
486
+ }
487
+ }
488
+ }
489
+ function groupReportOf(g) {
490
+ return { applied: g.next !== undefined ? [...APPROVAL_GROUP_KEYS] : [], ...(g.rejected ? { rejected: g.rejected } : {}) };
491
+ }
492
+ const approvalEnvBaseline = new WeakMap();
493
+ function stageApprovalGate(config, gov, leg, logger) {
494
+ if (!approvalEnvBaseline.has(config))
495
+ approvalEnvBaseline.set(config, Object.freeze([...config.approvalRequire]));
496
+ const refuse = (r) => {
497
+ logger?.warn("sema_registry_approval_require_invalid", { keys: [...APPROVAL_GROUP_KEYS], detail: r.detail, names: [...r.names], kept: r.kept });
498
+ return { rejected: { keys: [...APPROVAL_GROUP_KEYS], reason: r.reason } };
499
+ };
402
500
  const raw = gov?.approvalRequire;
403
- if (!Array.isArray(raw))
404
- return { assignments: [] };
405
- const staged = stageApprovalRequire(raw, logger);
406
- if (staged.value === undefined)
407
- return { assignments: [] };
408
- return { assignments: [["approvalRequire", staged.value]], info: { approvalRequire: staged.value } };
501
+ let next;
502
+ if (raw === undefined) {
503
+ next = [...approvalEnvBaseline.get(config)];
504
+ }
505
+ else {
506
+ const verdict = stageApprovalRequire(raw);
507
+ if (!verdict.ok)
508
+ return refuse(verdict);
509
+ next = verdict.value;
510
+ }
511
+ const unserviceable = unserviceableOnLeg(leg, { ...config, approvalRequire: next });
512
+ if (unserviceable !== undefined)
513
+ return refuse({ detail: unserviceable, reason: unserviceable, names: next, kept: KEPT_WOULD_NEVER_PARK });
514
+ const same = next.length === config.approvalRequire.length && next.every((n, i) => n === config.approvalRequire[i]);
515
+ return same ? {} : { next };
409
516
  }
410
517
  function legacyGateValues(rt) {
411
518
  const out = {};
@@ -416,16 +523,16 @@ function legacyGateValues(rt) {
416
523
  out[k] = rt[k];
417
524
  return out;
418
525
  }
419
- export function applyRuntimeGates(config, eff, logger) {
420
- const s = stageApprovalGate(eff?.governance, logger);
421
- for (const [k, v] of s.assignments)
422
- config[k] = v;
526
+ export function applyRuntimeGates(config, eff, leg, logger) {
527
+ const approval = stageApprovalGate(config, eff?.governance, leg, logger);
528
+ if (approval.next !== undefined)
529
+ config.approvalRequire = approval.next;
423
530
  const limits = stageLimits(config, { version: 0, legacyGates: legacyGateValues(eff?.runtime) }, logger);
424
531
  for (const [k, v] of limits.assignments)
425
532
  config[k] = v;
426
533
  for (const k of limits.deletions)
427
534
  delete config[k];
428
- const info = { ...s.info, ...Object.fromEntries(limits.assignments) };
535
+ const info = { ...(approval.next !== undefined ? { approvalRequire: approval.next } : {}), ...Object.fromEntries(limits.assignments) };
429
536
  if (Object.keys(info).length > 0)
430
537
  logger?.info("sema_registry_runtime", info);
431
538
  }
@@ -486,6 +593,8 @@ export function resolveDefaultModelName(eff, has, fallback, onDangling) {
486
593
  }
487
594
  export function logEffectiveDiff(config, eff, logger) {
488
595
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
596
+ const plane = stageModelPlane(config, eff, undefined, undefined);
597
+ const refused = plane.refused ?? {};
489
598
  const centerModels = enabled.map((m) => ({ name: m.name, id: m.id, provider: m.provider, apiKeyEnv: m.apiKeyEnv ?? null, tier: m.tier ?? null }));
490
599
  const envModels = Object.entries(config.models).map(([name, m]) => ({ name, id: m.id, provider: m.provider }));
491
600
  const centerRoles = Object.fromEntries(Object.entries(eff.models?.roles ?? {}).map(([r, t]) => [r, "model" in t ? t.model : { select: t.select }]));
@@ -493,7 +602,9 @@ export function logEffectiveDiff(config, eff, logger) {
493
602
  note: "DRY RUN — registry config NOT applied; unset SEMA_REGISTRY_DRY_RUN to go live",
494
603
  version: eff.version,
495
604
  wouldOverrideModels: enabled.length > 0,
496
- wouldDefaultModel: enabled.length > 0 ? resolveDefaultModelName(eff, (n) => enabled.some((m) => m.name === n), enabled[0].name).name : null,
605
+ wouldDefaultModel: plane.defaultName ?? null,
606
+ ...(Object.keys(refused).length > 0 ? { wouldRefuse: Object.fromEntries(Object.entries(refused).map(([n, r]) => [n, r.code])) } : {}),
607
+ ...(plane.rejected !== undefined ? { wouldRefuseModelPlane: { invariant: plane.rejected.invariant, reason: plane.rejected.reason } } : {}),
497
608
  currentDefaultModel: config.model.id,
498
609
  models: { center: centerModels, envDerived: envModels },
499
610
  roles: { center: centerRoles, current: config.roles },
@@ -20,7 +20,20 @@
20
20
  * 🔴 原子承诺的**域**是单进程(§2):本账记的是「本副本的收敛状态」,不是 fleet 的。fleet 级判据 = 各副本
21
21
  * /health 的 applied == target,拉齐工具归运维面。
22
22
  */
23
- /** 一组的收敛状态(批1 只有 limitsHttp 组;后续批次按组追加,读面形不变)。 */
23
+ /**
24
+ * 世代账按组记账的**组闭集**(读面 `groups` 的键集;加组 = 这里加词,报告形 `Record<组, …>` 随之编译红)。
25
+ * · `limitsHttp` —— [ref] 批1 限额组(属主 `stage-limits.ts`);
26
+ * · `approvalGate` —— S-668 波 2 审批组(`governance.approvalRequire`;属主 `apply-effective.ts` 的
27
+ * `stageApprovalGate`:坏形 / 工具名匹配不到 / ④ 门意图不可服务 ⇒ 整组拒)。
28
+ * · `modelPlane` —— 模型面组(组内有哪些 effective 域的**唯一定义** = `apply-effective.ts` 的 `MODEL_PLANE_DOMAINS`:models 域 =
29
+ * 目录 / roles / tiers / 拒因表 / 名单 / 钥匙表,collab 域 = 引用它的命名 workflow 注册表;属主 `stageModelPlane`:
30
+ * 模型面三条跨键不变量 `default-model-resolvable` / `model-route-endpoint` / `refused-names-not-minted` 任一不成立 ⇒ 整组拒,
31
+ * 组内各域都停在上一代。opus 腿 O3(7.101.0 合并复审):此前这三条是 stage 抛 = 整份候选中止,同候选其余组陪葬)。
32
+ * 各组同姿势、各用各的判据表(审批组**不**复用限额组的跨键不变量表 —— 那张表的候选视图只叠限额键)。
33
+ */
34
+ export declare const CONFIG_APPLY_GROUPS: readonly ["limitsHttp", "approvalGate", "modelPlane"];
35
+ export type ConfigApplyGroup = (typeof CONFIG_APPLY_GROUPS)[number];
36
+ /** 一组的收敛状态(读面形对所有组相同)。 */
24
37
  export interface ConfigApplyGroupState {
25
38
  /** 本组最后一次**真的落地**的源版本(身份标)。 */
26
39
  readonly appliedVersion: number;
@@ -48,27 +61,43 @@ export interface ConfigApplyLedgerView {
48
61
  readonly configAppliedOrdinal: number;
49
62
  /** target≠applied 的**持续**时长(ms);收敛时键缺席(健康面不制造噪音)。 */
50
63
  readonly configApplyStaleMs?: number;
51
- readonly groups: Readonly<Record<string, ConfigApplyGroupState>>;
64
+ readonly groups: Readonly<Partial<Record<ConfigApplyGroup, ConfigApplyGroupState>>>;
65
+ }
66
+ /** 一组在一次 apply 里的报告(所有组同形)。 */
67
+ export interface ConfigApplyGroupReport {
68
+ /** 本世代真正换了值的键。 */
69
+ readonly applied: readonly string[];
70
+ /** 值合法但**不热**的键(逐键 defer:变更只能重启生效)。 */
71
+ readonly deferred?: ReadonlyArray<{
72
+ readonly key: string;
73
+ readonly reason: string;
74
+ }>;
75
+ /** 整组被拒时的成因(消毒:键名 + 判据文案,绝不带值)。 */
76
+ readonly rejected?: {
77
+ readonly keys: readonly string[];
78
+ readonly reason: string;
79
+ readonly restartReason?: string;
80
+ };
52
81
  }
53
- /** applyEffective 的 post-commit 报告(见 apply-effective.ts `ApplyReport`)。 */
82
+ /** applyEffective 的 post-commit 报告(见 apply-effective.ts `ApplyReport`)—— **每一组都报**。 */
54
83
  export interface ConfigApplyReport {
55
84
  readonly version: number;
56
85
  /** [ref]:本候选的收货序数(applyEffective opts.generation 原样带回;缺席=version 兼容臂)。 */
57
86
  readonly ordinal?: number;
58
- readonly limits: {
59
- readonly applied: readonly string[];
60
- readonly deferred?: ReadonlyArray<{
61
- readonly key: string;
62
- readonly reason: string;
63
- }>;
64
- readonly rejected?: {
65
- readonly keys: readonly string[];
66
- readonly reason: string;
67
- readonly restartReason?: string;
68
- };
69
- };
70
- readonly fullyApplied?: boolean;
87
+ readonly groups: Readonly<Record<ConfigApplyGroup, ConfigApplyGroupReport>>;
71
88
  }
89
+ /** 本报告里被整组拒的那些组(按组闭集序)。空 ⇒ 候选完整应用。两条 commit 腿的「部分应用」warn 与世代账共用。 */
90
+ export declare function refusedGroups(report: ConfigApplyReport): Array<{
91
+ readonly group: ConfigApplyGroup;
92
+ readonly keys: readonly string[];
93
+ readonly reason: string;
94
+ }>;
95
+ /**
96
+ * O3:这一次 apply 的**模型面组**(组内各域 = `MODEL_PLANE_DOMAINS`,目录与引用它的 collab 模板同进退)落地了没有(整组被拒 ⇒
97
+ * false;报告缺席 = 没走到 commit ⇒ false,如实)。调用方据此决定「Runner 现在跑的是哪一代模型面」(`appliedPlaneEff`,swap 判据的
98
+ * 基准)、名册门开不开、以及 boot 要不要按组回落 LKG —— 都只许从真落地的模型面推进。
99
+ */
100
+ export declare function modelPlaneCommitted(report: ConfigApplyReport | undefined): boolean;
72
101
  export interface ConfigApplyLedger {
73
102
  /** 拉到一个候选(无论后续是否应用)。ordinal=收货序数,version=身份标。 */
74
103
  seenTarget(ordinal: number, version: number): void;