@sema-agent/server 7.99.0 → 7.100.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 (115) hide show
  1. package/MIGRATION.md +13 -0
  2. package/USAGE.md +75 -52
  3. package/dist/approval-ask-machine.d.ts +10 -0
  4. package/dist/approval-ask-machine.js +3 -0
  5. package/dist/approval-card.d.ts +74 -1
  6. package/dist/approval-card.js +41 -8
  7. package/dist/approval.d.ts +44 -20
  8. package/dist/approval.js +14 -6
  9. package/dist/boot/config-center.d.ts +16 -8
  10. package/dist/boot/config-center.js +39 -81
  11. package/dist/boot/execution-env.d.ts +1 -2
  12. package/dist/boot/execution-env.js +2 -2
  13. package/dist/boot/leader.js +12 -9
  14. package/dist/boot/memory-consolidation.d.ts +38 -0
  15. package/dist/boot/memory-consolidation.js +35 -0
  16. package/dist/boot/resolve-spec.d.ts +0 -1
  17. package/dist/boot/resolve-spec.js +4 -3
  18. package/dist/boot/runner-deps.d.ts +2 -2
  19. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  20. package/dist/boot/stage-06-runners.d.ts +2 -2
  21. package/dist/boot/stage-06-runners.js +3 -3
  22. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  23. package/dist/boot/stage-07-capability-layer.js +6 -6
  24. package/dist/boot/stage-08-reapers.d.ts +3 -4
  25. package/dist/boot/stage-08-reapers.js +2 -2
  26. package/dist/boot/stage-09-leader.d.ts +2 -3
  27. package/dist/boot/stage-10-http-server.d.ts +2 -3
  28. package/dist/boot/stage-10-http-server.js +2 -2
  29. package/dist/capabilities/center-plugins.d.ts +14 -2
  30. package/dist/capabilities/center-plugins.js +13 -5
  31. package/dist/capabilities/hands-lane.js +2 -1
  32. package/dist/capabilities/scenarios.js +18 -5
  33. package/dist/capabilities/tool-defer.d.ts +2 -2
  34. package/dist/config-catalog.d.ts +2 -2
  35. package/dist/config-catalog.js +12 -13
  36. package/dist/config-center/apply-effective.d.ts +34 -39
  37. package/dist/config-center/apply-effective.js +100 -64
  38. package/dist/config-center/apply-ledger.d.ts +35 -16
  39. package/dist/config-center/apply-ledger.js +25 -18
  40. package/dist/config-center/facade.d.ts +11 -5
  41. package/dist/config-center/facade.js +1 -1
  42. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  43. package/dist/config-center/hot-keys-registry.js +11 -7
  44. package/dist/config-center/restart-signal.d.ts +21 -14
  45. package/dist/config-center/restart-signal.js +16 -22
  46. package/dist/config-center/types.d.ts +3 -3
  47. package/dist/config-types.d.ts +22 -7
  48. package/dist/cross-session-settings.js +1 -1
  49. package/dist/host-lsp-manager.d.ts +29 -0
  50. package/dist/host-lsp-manager.js +14 -0
  51. package/dist/http/active-run-conflict.d.ts +2 -12
  52. package/dist/http/active-run-conflict.js +1 -6
  53. package/dist/http/admission.js +8 -0
  54. package/dist/http/resume-legs.d.ts +1 -1
  55. package/dist/http/resume-legs.js +14 -10
  56. package/dist/http/route-ctx.d.ts +2 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  58. package/dist/http/routes/approvals-assistant.js +2 -2
  59. package/dist/http/routes/memory-compliance.js +5 -0
  60. package/dist/http/routes/memory-origin.js +5 -0
  61. package/dist/http/routes/sessions.js +6 -0
  62. package/dist/http/routes/tasks.js +7 -6
  63. package/dist/http/server.d.ts +6 -5
  64. package/dist/http/wire-types.d.ts +6 -3
  65. package/dist/leader/wire.d.ts +20 -12
  66. package/dist/leader/wire.js +7 -6
  67. package/dist/memory-layer-preflight.d.ts +46 -0
  68. package/dist/memory-layer-preflight.js +121 -0
  69. package/dist/memory-operator-faces.js +17 -1
  70. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  71. package/dist/observability/corrupt-read-seat.js +31 -0
  72. package/dist/observability/fail-open.d.ts +8 -4
  73. package/dist/observability/fail-open.js +8 -4
  74. package/dist/observability/run-terminal.js +1 -0
  75. package/dist/observability/secret-env-scrub.d.ts +1 -0
  76. package/dist/observability/secret-env-scrub.js +1 -0
  77. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  78. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  79. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  80. package/dist/plugins/approval-ask-store-file.js +21 -3
  81. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  82. package/dist/plugins/approval-ask-store-memory.js +39 -13
  83. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  84. package/dist/plugins/approval-ask-store-sql.js +27 -3
  85. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  86. package/dist/plugins/checkpoint-store-sql.js +19 -2
  87. package/dist/plugins/local-checkpoint-store.js +2 -0
  88. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  89. package/dist/plugins/mailbox-store-sql.js +6 -4
  90. package/dist/plugins/permission-rule-store-file.js +3 -2
  91. package/dist/plugins/sql-json-column.d.ts +16 -3
  92. package/dist/plugins/sql-json-column.js +15 -4
  93. package/dist/plugins/web-search.d.ts +42 -22
  94. package/dist/plugins/web-search.js +141 -49
  95. package/dist/run-local.js +6 -5
  96. package/dist/runs.js +6 -5
  97. package/dist/runtime-governance.d.ts +50 -3
  98. package/dist/runtime-governance.js +5 -0
  99. package/dist/server-secret-env.d.ts +4 -2
  100. package/dist/task-settings.d.ts +1 -1
  101. package/dist/tool-approval.d.ts +107 -19
  102. package/dist/tool-approval.js +64 -15
  103. package/dist/trace/core-keyset-guard.d.ts +5 -5
  104. package/dist/trace/ledger-sink.js +2 -2
  105. package/dist/trace/project.d.ts +13 -0
  106. package/dist/trace/project.js +22 -5
  107. package/dist/trace/projection-drop.d.ts +2 -0
  108. package/dist/trace/projection-drop.js +6 -0
  109. package/dist/trace/sema-provenance.d.ts +4 -1
  110. package/dist/trace/sema-provenance.js +1 -1
  111. package/dist/trace/task-notification-facets.d.ts +65 -0
  112. package/dist/trace/task-notification-facets.js +31 -0
  113. package/dist/trace/wire-projection-faces.d.ts +3 -3
  114. package/dist/trace/wire-projection-faces.js +3 -3
  115. package/package.json +3 -3
@@ -5,9 +5,11 @@ 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 { gateIntentUnserviceableReason } from "../approval.js";
12
+ import { refusedGroups } from "./apply-ledger.js";
11
13
  const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
12
14
  function envDerivedModel(config) {
13
15
  const m = config.envModel;
@@ -93,7 +95,7 @@ function stageAtModelAllowlist(raw, models, logger) {
93
95
  }
94
96
  return names;
95
97
  }
96
- export function applyEffective(config, eff, logger, opts = {}) {
98
+ export function applyEffective(config, eff, logger, opts) {
97
99
  const staged = stageEffective(config, eff, logger, opts);
98
100
  const gen = opts.generation ?? staged.version;
99
101
  const live = appliedGeneration.get(config);
@@ -118,25 +120,30 @@ export function applyEffective(config, eff, logger, opts = {}) {
118
120
  commitStaged(config, staged);
119
121
  if (ordered)
120
122
  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
- });
123
+ if (opts.report) {
124
+ const report = {
125
+ version: staged.version,
126
+ ...(opts.generation !== undefined ? { ordinal: opts.generation } : {}),
127
+ groups: {
128
+ limitsHttp: {
129
+ applied: staged.limits.changed,
130
+ ...(staged.limits.deferred.length > 0 ? { deferred: staged.limits.deferred } : {}),
131
+ ...(staged.limits.rejected ? { rejected: staged.limits.rejected } : {}),
132
+ },
133
+ approvalGate: groupReportOf(staged.approvalGate),
134
+ },
135
+ };
136
+ opts.report({ ...report, fullyApplied: refusedGroups(report).length === 0 });
137
+ }
131
138
  if (staged.limits.changed.length > 0)
132
139
  logger?.info("sema_registry_limits", { version: staged.version, keys: staged.limits.changed });
133
140
  if (staged.modelPlane)
134
141
  logger?.info("sema_registry_models", staged.modelPlane.infoLine);
135
- if (staged.gatesInfo)
136
- logger?.info("sema_registry_runtime", staged.gatesInfo);
142
+ if (staged.approvalGate.next !== undefined)
143
+ logger?.info("sema_registry_runtime", { version: staged.version, approvalRequire: staged.approvalGate.next });
137
144
  if (Object.keys(staged.hot.infoLine).length > 0)
138
145
  logger?.info("sema_registry_runtime_hot", staged.hot.infoLine);
139
- for (const n of staged.collabPlane?.notices ?? []) {
146
+ for (const n of staged.collabPlane.notices) {
140
147
  if (n.level === "warn")
141
148
  logger?.warn(n.msg, n.fields);
142
149
  else
@@ -158,10 +165,8 @@ function commitStaged(config, staged) {
158
165
  mutateInPlace(config.tiers, mp.activeTiers);
159
166
  config.atModelAllowlist = mp.atModelAllowlist;
160
167
  }
161
- if (staged.gateAssignments) {
162
- for (const [k, v] of staged.gateAssignments)
163
- config[k] = v;
164
- }
168
+ if (staged.approvalGate.next !== undefined)
169
+ config.approvalRequire = staged.approvalGate.next;
165
170
  for (const [k, v] of staged.limits.assignments)
166
171
  config[k] = v;
167
172
  for (const k of staged.limits.deletions)
@@ -170,12 +175,11 @@ function commitStaged(config, staged) {
170
175
  config.autonomy = staged.hot.autonomyNext;
171
176
  if (staged.hot.setCommandPolicy)
172
177
  config.commandPolicy = staged.hot.commandPolicyNext;
173
- if (staged.collabPlane)
174
- registerCollabWorkflows(staged.collabPlane.workflows);
178
+ registerCollabWorkflows(staged.collabPlane.workflows);
175
179
  mutateInPlace(config.projects, staged.projects);
176
180
  }
177
181
  function stageEffective(config, eff, logger, opts) {
178
- if (!opts.teamsOnly && !eff.version) {
182
+ if (opts.leg === "boot" && !eff.version) {
179
183
  logger?.warn("sema_registry_unpublished", {
180
184
  version: eff.version,
181
185
  note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
@@ -183,7 +187,7 @@ function stageEffective(config, eff, logger, opts) {
183
187
  }
184
188
  const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
185
189
  let modelPlane;
186
- if (enabled.length > 0 && opts.deferModelPlane !== true) {
190
+ if (enabled.length > 0) {
187
191
  const models = {};
188
192
  const modelApiKeyEnv = {};
189
193
  const modelApiKeys = {};
@@ -282,21 +286,18 @@ function stageEffective(config, eff, logger, opts) {
282
286
  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 },
283
287
  };
284
288
  }
285
- const gates = opts.teamsOnly ? undefined : stageApprovalGate(eff.governance, logger);
289
+ const approvalGate = stageApprovalGate(config, eff.governance, opts, logger);
286
290
  const limits = stageLimits(config, { version: typeof eff.version === "number" ? eff.version : 0, ...("limits" in eff ? { limits: eff.limits } : {}), legacyGates: legacyGateValues(eff.runtime) }, logger);
287
291
  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
- }
292
+ const projected = projectCollabToWorkflows(eff.collab?.templates);
293
+ const collabNotices = [];
294
+ if (Object.keys(projected.workflows).length > 0)
295
+ collabNotices.push({ level: "info", msg: "sema_registry_collab_workflows", fields: { workflows: Object.keys(projected.workflows) } });
296
+ if (projected.skipped.length > 0)
297
+ collabNotices.push({ level: "warn", msg: "sema_registry_collab_skipped", fields: { skipped: projected.skipped } });
298
+ if (projected.notes.length > 0)
299
+ collabNotices.push({ level: "info", msg: "sema_registry_collab_notes", fields: { notes: projected.notes } });
300
+ const collabPlane = { workflows: projected.workflows, notices: collabNotices };
300
301
  const projDomain = eff.projects;
301
302
  const rawProjects = (projDomain && typeof projDomain === "object" && !Array.isArray(projDomain) &&
302
303
  "projects" in projDomain && typeof projDomain.projects === "object"
@@ -316,15 +317,14 @@ function stageEffective(config, eff, logger, opts) {
316
317
  return {
317
318
  version: typeof eff.version === "number" && Number.isFinite(eff.version) ? eff.version : 0,
318
319
  ...(modelPlane !== undefined ? { modelPlane } : {}),
319
- ...(gates !== undefined ? { gateAssignments: gates.assignments, ...(gates.info !== undefined ? { gatesInfo: gates.info } : {}) } : {}),
320
+ approvalGate,
320
321
  limits,
321
322
  hot,
322
- ...(collabPlane !== undefined ? { collabPlane } : {}),
323
+ collabPlane,
323
324
  projects,
324
325
  };
325
326
  }
326
327
  export const RUNTIME_GATE_KEYS = ["rateLimitPerMin", "maxTaskCostUsd", "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec"];
327
- export const BOOT_GATE_KEYS = ["approvalRequire"];
328
328
  export function centerManagedConfigKeys(eff) {
329
329
  const out = new Set();
330
330
  if (!eff)
@@ -372,40 +372,76 @@ export function runtimeGatePresent(rt, key) {
372
372
  return typeof v === "number" && v > 0;
373
373
  return typeof v === "number";
374
374
  }
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";
375
+ const KEPT_WOULD_MATCH_NOTHING = "the prior approvalRequire is unchanged — the published list would have silently matched nothing";
376
+ const KEPT_WOULD_REMOVE_GATE = "the prior approvalRequire is unchanged — coercing this malformed value would have silently REMOVED the approval gate";
377
+ 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";
378
+ function stageApprovalRequire(raw) {
382
379
  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);
380
+ const detail = `published governance.approvalRequire must be an ARRAY of whole tool names — got ${raw === null ? "null" : typeof raw}. ` +
381
+ `Refusing the group instead of coercing it: an empty list means "no approval gate", so coercing a malformed value would SILENTLY remove the gate.`;
382
+ return { ok: false, detail, reason: detail, names: [], kept: KEPT_WOULD_REMOVE_GATE };
385
383
  }
386
384
  const nonString = raw
387
385
  .map((n, i) => (typeof n === "string" ? undefined : { i, t: n === null ? "null" : typeof n }))
388
386
  .filter((e) => e !== undefined);
389
387
  if (nonString.length > 0) {
390
- return refuse(`published runtime.approvalRequire contains ${nonString.length} non-string entr${nonString.length === 1 ? "y" : "ies"} ` +
388
+ const detail = `published governance.approvalRequire contains ${nonString.length} non-string entr${nonString.length === 1 ? "y" : "ies"} ` +
391
389
  `(${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);
390
+ `Refusing the group instead of dropping them: dropping every entry would SILENTLY remove the approval gate.`;
391
+ return { ok: false, detail, reason: detail, names: nonString.map((e) => `[${e.i}]`), kept: KEPT_WOULD_REMOVE_GATE };
393
392
  }
394
393
  const names = raw.filter((n) => typeof n === "string").map((n) => n.trim()).filter((n) => n.length > 0);
395
394
  const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
396
395
  if (bad.length > 0) {
397
- return refuse(formatUnmatchableToolNames("published runtime.approvalRequire", bad), bad.map((b) => b.name), KEPT_WOULD_MATCH_NOTHING);
396
+ return {
397
+ ok: false,
398
+ detail: formatUnmatchableToolNames("published governance.approvalRequire", bad),
399
+ 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`,
400
+ names: bad.map((b) => b.name),
401
+ kept: KEPT_WOULD_MATCH_NOTHING,
402
+ };
398
403
  }
399
- return { value: names };
404
+ return { ok: true, value: names };
400
405
  }
401
- function stageApprovalGate(gov, logger) {
406
+ function unserviceableOnLeg(leg, candidate) {
407
+ switch (leg.leg) {
408
+ case "boot":
409
+ return undefined;
410
+ case "hot":
411
+ return gateIntentUnserviceableReason(candidate, leg.checkpointStorePresent);
412
+ default: {
413
+ const unknownLeg = leg;
414
+ throw new Error(`applyEffective: unknown apply leg ${JSON.stringify(unknownLeg)}`);
415
+ }
416
+ }
417
+ }
418
+ function groupReportOf(g) {
419
+ return { applied: g.next !== undefined ? [...APPROVAL_GROUP_KEYS] : [], ...(g.rejected ? { rejected: g.rejected } : {}) };
420
+ }
421
+ const approvalEnvBaseline = new WeakMap();
422
+ function stageApprovalGate(config, gov, leg, logger) {
423
+ if (!approvalEnvBaseline.has(config))
424
+ approvalEnvBaseline.set(config, Object.freeze([...config.approvalRequire]));
425
+ const refuse = (r) => {
426
+ logger?.warn("sema_registry_approval_require_invalid", { keys: [...APPROVAL_GROUP_KEYS], detail: r.detail, names: [...r.names], kept: r.kept });
427
+ return { rejected: { keys: [...APPROVAL_GROUP_KEYS], reason: r.reason } };
428
+ };
402
429
  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 } };
430
+ let next;
431
+ if (raw === undefined) {
432
+ next = [...approvalEnvBaseline.get(config)];
433
+ }
434
+ else {
435
+ const verdict = stageApprovalRequire(raw);
436
+ if (!verdict.ok)
437
+ return refuse(verdict);
438
+ next = verdict.value;
439
+ }
440
+ const unserviceable = unserviceableOnLeg(leg, { ...config, approvalRequire: next });
441
+ if (unserviceable !== undefined)
442
+ return refuse({ detail: unserviceable, reason: unserviceable, names: next, kept: KEPT_WOULD_NEVER_PARK });
443
+ const same = next.length === config.approvalRequire.length && next.every((n, i) => n === config.approvalRequire[i]);
444
+ return same ? {} : { next };
409
445
  }
410
446
  function legacyGateValues(rt) {
411
447
  const out = {};
@@ -416,16 +452,16 @@ function legacyGateValues(rt) {
416
452
  out[k] = rt[k];
417
453
  return out;
418
454
  }
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;
455
+ export function applyRuntimeGates(config, eff, leg, logger) {
456
+ const approval = stageApprovalGate(config, eff?.governance, leg, logger);
457
+ if (approval.next !== undefined)
458
+ config.approvalRequire = approval.next;
423
459
  const limits = stageLimits(config, { version: 0, legacyGates: legacyGateValues(eff?.runtime) }, logger);
424
460
  for (const [k, v] of limits.assignments)
425
461
  config[k] = v;
426
462
  for (const k of limits.deletions)
427
463
  delete config[k];
428
- const info = { ...s.info, ...Object.fromEntries(limits.assignments) };
464
+ const info = { ...(approval.next !== undefined ? { approvalRequire: approval.next } : {}), ...Object.fromEntries(limits.assignments) };
429
465
  if (Object.keys(info).length > 0)
430
466
  logger?.info("sema_registry_runtime", info);
431
467
  }
@@ -20,7 +20,16 @@
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
+ * 两组同姿势、各用各的判据表(审批组**不**复用限额组的跨键不变量表 —— 那张表的候选视图只叠限额键)。
29
+ */
30
+ export declare const CONFIG_APPLY_GROUPS: readonly ["limitsHttp", "approvalGate"];
31
+ export type ConfigApplyGroup = (typeof CONFIG_APPLY_GROUPS)[number];
32
+ /** 一组的收敛状态(读面形对所有组相同)。 */
24
33
  export interface ConfigApplyGroupState {
25
34
  /** 本组最后一次**真的落地**的源版本(身份标)。 */
26
35
  readonly appliedVersion: number;
@@ -48,27 +57,37 @@ export interface ConfigApplyLedgerView {
48
57
  readonly configAppliedOrdinal: number;
49
58
  /** target≠applied 的**持续**时长(ms);收敛时键缺席(健康面不制造噪音)。 */
50
59
  readonly configApplyStaleMs?: number;
51
- readonly groups: Readonly<Record<string, ConfigApplyGroupState>>;
60
+ readonly groups: Readonly<Partial<Record<ConfigApplyGroup, ConfigApplyGroupState>>>;
61
+ }
62
+ /** 一组在一次 apply 里的报告(所有组同形)。 */
63
+ export interface ConfigApplyGroupReport {
64
+ /** 本世代真正换了值的键。 */
65
+ readonly applied: readonly string[];
66
+ /** 值合法但**不热**的键(逐键 defer:变更只能重启生效)。 */
67
+ readonly deferred?: ReadonlyArray<{
68
+ readonly key: string;
69
+ readonly reason: string;
70
+ }>;
71
+ /** 整组被拒时的成因(消毒:键名 + 判据文案,绝不带值)。 */
72
+ readonly rejected?: {
73
+ readonly keys: readonly string[];
74
+ readonly reason: string;
75
+ readonly restartReason?: string;
76
+ };
52
77
  }
53
- /** applyEffective 的 post-commit 报告(见 apply-effective.ts `ApplyReport`)。 */
78
+ /** applyEffective 的 post-commit 报告(见 apply-effective.ts `ApplyReport`)—— **每一组都报**。 */
54
79
  export interface ConfigApplyReport {
55
80
  readonly version: number;
56
81
  /** [ref]:本候选的收货序数(applyEffective opts.generation 原样带回;缺席=version 兼容臂)。 */
57
82
  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;
83
+ readonly groups: Readonly<Record<ConfigApplyGroup, ConfigApplyGroupReport>>;
71
84
  }
85
+ /** 本报告里被整组拒的那些组(按组闭集序)。空 ⇒ 候选完整应用。两条 commit 腿的「部分应用」warn 与世代账共用。 */
86
+ export declare function refusedGroups(report: ConfigApplyReport): Array<{
87
+ readonly group: ConfigApplyGroup;
88
+ readonly keys: readonly string[];
89
+ readonly reason: string;
90
+ }>;
72
91
  export interface ConfigApplyLedger {
73
92
  /** 拉到一个候选(无论后续是否应用)。ordinal=收货序数,version=身份标。 */
74
93
  seenTarget(ordinal: number, version: number): void;
@@ -1,3 +1,10 @@
1
+ export const CONFIG_APPLY_GROUPS = ["limitsHttp", "approvalGate"];
2
+ export function refusedGroups(report) {
3
+ return CONFIG_APPLY_GROUPS.flatMap((group) => {
4
+ const r = report.groups[group].rejected;
5
+ return r ? [{ group, keys: r.keys, reason: r.reason }] : [];
6
+ });
7
+ }
1
8
  export function createConfigApplyLedger(now = () => Date.now()) {
2
9
  let targetOrd = 0;
3
10
  let appliedOrd = 0;
@@ -6,6 +13,21 @@ export function createConfigApplyLedger(now = () => Date.now()) {
6
13
  const rejectedOrds = new Set();
7
14
  let divergedSince;
8
15
  const groups = {};
16
+ const nextGroupState = (prev, r, version) => {
17
+ if (r.rejected) {
18
+ return {
19
+ appliedVersion: prev?.appliedVersion ?? 0,
20
+ ...(prev?.deferredKeys ? { deferredKeys: prev.deferredKeys } : {}),
21
+ lastRejected: { keys: [...r.rejected.keys], reason: r.rejected.reason, version, at: now() },
22
+ ...(r.rejected.restartReason ? { deferredKeys: r.rejected.keys.map((key) => ({ key, reason: r.rejected.restartReason })) } : {}),
23
+ };
24
+ }
25
+ return {
26
+ appliedVersion: version,
27
+ ...(r.deferred && r.deferred.length > 0 ? { deferredKeys: r.deferred.map((d) => ({ key: d.key, reason: d.reason })) } : {}),
28
+ ...(prev?.lastRejected ? { lastRejected: prev.lastRejected } : {}),
29
+ };
30
+ };
9
31
  const reconcile = () => {
10
32
  if (targetOrd === appliedOrd)
11
33
  divergedSince = undefined;
@@ -32,28 +54,13 @@ export function createConfigApplyLedger(now = () => Date.now()) {
32
54
  reconcile();
33
55
  },
34
56
  recordApply(report) {
35
- const prev = groups.limitsHttp;
36
57
  const ord = report.ordinal ?? report.version;
37
- if (report.limits.rejected)
58
+ if (refusedGroups(report).length > 0)
38
59
  rejectedOrds.add(ord);
39
60
  else
40
61
  rejectedOrds.delete(ord);
41
- if (report.limits.rejected) {
42
- groups.limitsHttp = {
43
- appliedVersion: prev?.appliedVersion ?? 0,
44
- ...(prev?.deferredKeys ? { deferredKeys: prev.deferredKeys } : {}),
45
- lastRejected: { keys: [...report.limits.rejected.keys], reason: report.limits.rejected.reason, version: report.version, at: now() },
46
- ...(report.limits.rejected.restartReason
47
- ? { deferredKeys: report.limits.rejected.keys.map((key) => ({ key, reason: report.limits.rejected.restartReason })) }
48
- : {}),
49
- };
50
- return;
51
- }
52
- groups.limitsHttp = {
53
- appliedVersion: report.version,
54
- ...(report.limits.deferred && report.limits.deferred.length > 0 ? { deferredKeys: report.limits.deferred.map((d) => ({ key: d.key, reason: d.reason })) } : {}),
55
- ...(prev?.lastRejected ? { lastRejected: prev.lastRejected } : {}),
56
- };
62
+ for (const g of CONFIG_APPLY_GROUPS)
63
+ groups[g] = nextGroupState(groups[g], report.groups[g], report.version);
57
64
  },
58
65
  view() {
59
66
  const stale = divergedSince !== undefined ? now() - divergedSince : undefined;
@@ -25,14 +25,20 @@
25
25
  * reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
26
26
  * call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
27
27
  * next task with no restart.
28
- * - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
29
- * at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
30
- * - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
28
+ * - tiers (and the whole plane under an active tier table) — **HOT** as well: core expands a PRIVATE catalog
29
+ * copy at Runner construction, and the refresh loop swaps every changed plane into all boot Runners before
30
+ * commit (`Runner.swapDeps`, core 7.10.0; production always wires it). The old "tier-frozen ⇒ defer to restart"
31
+ * arm and its `models-tiers` restart slice retired in S-668 波 0 — `/v1/models` and tier-word routing move on
32
+ * the same tick.
33
+ * - **`degrade-route`** — one of the two genuinely boot-frozen catalog consumers left (the other is the
34
+ * memory-consolidation driver seat, last bullet): reactive degrade
31
35
  * (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
32
36
  * at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
33
37
  * restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
34
38
  * gateway silently on the rate-limit path.
35
- * - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
39
+ * - skills(+plugins)/mcp/a2a/scenarios — restart-to-apply by construction (baked into the boot
40
+ * wiring); so is the memory-consolidation driver's model seat (`memory-consolidation` slice, S-668 波 0).
41
+ * (`governance.approvalRequire` left this list in S-668 波 2 — its own staged group, hot on both legs.)
36
42
  *
37
43
  * ([ref] A13, internal-lossless) This module is now a FACADE: the implementation lives
38
44
  * alongside it in `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig
@@ -43,7 +49,7 @@
43
49
  */
44
50
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
45
51
  export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, } from "./apply-effective.js";
46
- export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
52
+ export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
47
53
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
48
54
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
49
55
  export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterModel, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
@@ -1,6 +1,6 @@
1
1
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
2
2
  export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, } from "./apply-effective.js";
3
- export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./restart-signal.js";
3
+ export { restartReasons, modelPlaneChanged, } from "./restart-signal.js";
4
4
  export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
5
5
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";
6
6
  //# sourceMappingURL=facade.js.map
@@ -6,7 +6,7 @@
6
6
  * 键 → 组 → 换代手法 → **参数化用例 ID**(不是文件名——文件名粒度挡不住「文件在、用例没了」)。
7
7
  *
8
8
  * 机器门在 test/config-hot/batch1-hot-keys-registry.test.ts:登记为热却没有三断言用例 = 红;
9
- * 表的键集与 stage 真正消费的键集(`BATCH1_LIMIT_KEYS`)不等 = 红。
9
+ * 表的键集与 stage 真正消费的键集(`BATCH1_LIMIT_KEYS` ∪ `APPROVAL_GROUP_KEYS`)不等 = 红。
10
10
  *
11
11
  * ⚠️ 本表**只登记已落地的批**。批2-4 的键在各自批次里追加(设计稿 §6),不提前写进来 —— 一个「登记为热
12
12
  * 但没接线」的条目正是本表要消灭的那种谎。
@@ -27,8 +27,8 @@ export interface HotKeyEntry {
27
27
  readonly key: keyof ServiceConfigFlat;
28
28
  readonly group: keyof ServiceConfigGroups;
29
29
  readonly form: HotKeyForm;
30
- /** 落地批次(设计稿 §6 的批号)。 */
31
- readonly batch: 1;
30
+ /** 落地批次:1 = [ref] 批1(limitsHttp 组);2 = S-668 波 2(审批组 —— [ref] §6 原排的「批2」)。 */
31
+ readonly batch: 1 | 2;
32
32
  readonly cases: HotKeyCases;
33
33
  }
34
34
  /**
@@ -57,6 +57,13 @@ export declare const DEFERRED_LIMIT_KEYS: ReadonlyArray<{
57
57
  readonly restartReason: string;
58
58
  readonly why: string;
59
59
  }>;
60
- /** 热键表 = 批1 消费键集**减去**显式 deferred 的那些(表只登记真的热的键)。 */
60
+ /**
61
+ * S-668 波 2([ref] §3)—— **审批组**真正接线的键:`apply-effective.ts` 的 `stageApprovalGate` 消费、报告(世代账
62
+ * `groups.approvalGate`)与拒绝(拒因的键名)都取这一张表。换代手法 A′:消费点本就逐请求现读活 config(审批基线 /
63
+ * SendUserFile 子代闸 / 会话与记忆读面 / parked 赎回),leader 腿与单用户基线改成现读之后,热只差 refresh 腿的写口。
64
+ */
65
+ export declare const APPROVAL_GROUP_KEYS: readonly ["approvalRequire"];
66
+ export type ApprovalGroupKey = (typeof APPROVAL_GROUP_KEYS)[number];
67
+ /** 热键表 = 批1 消费键集**减去**显式 deferred 的那些 + 审批组(表只登记真的热的键)。 */
61
68
  export declare const HOT_KEYS_REGISTRY: readonly HotKeyEntry[];
62
69
  //# sourceMappingURL=hot-keys-registry.d.ts.map
@@ -29,11 +29,15 @@ export const DEFERRED_LIMIT_KEYS = [
29
29
  },
30
30
  ];
31
31
  const DEFERRED_KEYS = new Set(DEFERRED_LIMIT_KEYS.map((d) => d.key));
32
- export const HOT_KEYS_REGISTRY = BATCH1_LIMIT_KEYS.filter((key) => !DEFERRED_KEYS.has(key)).map((key) => ({
33
- key,
34
- group: "limitsHttp",
35
- form: (B_SETTER_KEYS.includes(key) ? "B-setter" : "A-prime"),
36
- batch: 1,
37
- cases: cases(key),
38
- }));
32
+ export const APPROVAL_GROUP_KEYS = ["approvalRequire"];
33
+ export const HOT_KEYS_REGISTRY = [
34
+ ...BATCH1_LIMIT_KEYS.filter((key) => !DEFERRED_KEYS.has(key)).map((key) => ({
35
+ key,
36
+ group: "limitsHttp",
37
+ form: (B_SETTER_KEYS.includes(key) ? "B-setter" : "A-prime"),
38
+ batch: 1,
39
+ cases: cases(key),
40
+ })),
41
+ ...APPROVAL_GROUP_KEYS.map((key) => ({ key, group: "approval", form: "A-prime", batch: 2, cases: cases(key) })),
42
+ ];
39
43
  //# sourceMappingURL=hot-keys-registry.js.map
@@ -1,5 +1,5 @@
1
1
  import type { EffectiveConfig } from "./types.js";
2
- export type RestartSlice = "skills" | "mcp" | "a2a" | "scenarios" | "runtime-gates" | "models-tiers" | "degrade-route" | "read-face";
2
+ export type RestartSlice = "skills" | "mcp" | "a2a" | "scenarios" | "degrade-route" | "read-face" | "memory-consolidation";
3
3
  /** What the CALLER must contribute about boot-baked catalog consumers that live OUTSIDE the Runner — the
4
4
  * slices cannot see them, because they are configured from env (`ServiceConfig`), not from the center's
5
5
  * EffectiveConfig. Absent/empty = that lane is off ⇒ its slice is inert (null on both sides). */
@@ -18,6 +18,18 @@ export interface RestartSliceCtx {
18
18
  * 新增/改址/域整体出现或消失仍签(新 mount / 改址只能 restart 才生效)。缺席/false = 旧语义照签
19
19
  * (fail-closed:没接席的部署里删除向没人承接,restart 是它唯一的生效路径)。 */
20
20
  mcpRevocationLive?: boolean;
21
+ /** S-668 波 0(过渡;**波 1 转热后删**):记忆整理驱动席的两侧读数 —— `baked` = boot 期 core 解析器选中的
22
+ * 那只 Model 的指纹,`current` = 同一只解析器对本拍活配置再解一次的指纹(读法见
23
+ * boot/memory-consolidation.ts `createConsolidationSeatWatch`)。缺席 = 阀门关着(`MEMORY_CONSOLIDATION_DRIVER`
24
+ * 不是 on,或记忆引擎没接线)⇒ 没有 boot 烤死的席 ⇒ `memory-consolidation` 片恒不签。 */
25
+ consolidationSeat?: {
26
+ readonly baked: unknown;
27
+ readonly current: unknown;
28
+ };
29
+ /** S-668 波 0(codex r1 [medium]):插件 host 白名单(env `PLUGINS_ALLOW_HOSTS`,boot 常量)—— 白名单外的声明
30
+ * 装载器整条跳过,它们的变化不是重启理由。生产 `restartCtx` 恒传 `config.pluginsAllowHosts`;缺席(单元调用)
31
+ * = 装载器的出厂名单 `DEFAULT_PLUGIN_ALLOW_HOSTS`。 */
32
+ pluginsAllowHosts?: readonly string[];
21
33
  }
22
34
  /** Structured restart signal an orchestrator consumes (GET /health → `restart`). Change-detected against the
23
35
  * process's BOOT config, NOT presence — so it stays absent when nothing baked-at-boot changed (presence would
@@ -46,18 +58,13 @@ export declare function stableStringify(v: unknown): string;
46
58
  * can auto rolling-restart on a non-empty result WITHOUT a restart loop, because the comparison is always
47
59
  * against boot (a refresh that re-fires the same diff is idempotent, not a fresh trigger). */
48
60
  export declare function restartReasons(boot: EffectiveConfig | undefined, current: EffectiveConfig, ctx?: RestartSliceCtx): RestartSlice[];
49
- /** codex R10 (models-tiers 窗收口): TRUE when the MODEL PLANE (enabled models + active tier table + resolved
50
- * default) of `next` differs from the last-APPLIED plane `prev`. Under a tier-frozen Runner (tiers non-empty at
51
- * construction → private expanded copy) main.ts DEFERS the whole plane mutation until restart when this is true —
52
- * admission (/v1/models, the catalog gates) and the Runner then stay on ONE generation through the restart
53
- * window, so a same-key retarget can never silently route/bill the stale Model object. `prev === undefined`
54
- * (env-boot plane, no EffectiveConfig to compare) is conservatively "changed" — one deferred adoption + restart
55
- * converges. Deliberately NOT gated on tier activity of either side (unlike the restart slice): a frozen Runner
56
- * goes stale on ANY plane change, including a tiers-off flip. */
57
- /** codex R12: TRUE when this candidate resolves an ACTIVE tier table. Used by main.ts's defer predicate — a
58
- * tier-less-booted Runner never re-runs expandTiers, so hot-applying an ARRIVING tier table splits admission
59
- * (tier words pass the expanded gate) from execution (core throws "Unknown model ref"). Hot-apply of the model
60
- * plane is safe only when BOTH the constructed generation and the candidate are tier-less. */
61
- export declare function planeHasActiveTiers(eff: EffectiveConfig): boolean;
61
+ /** The refresh loop's SWAP gate: TRUE when the MODEL PLANE (enabled models + active tier table + resolved
62
+ * default) of `next` differs from `prev`, the last plane handed to `swapRunnerDeps` — only then does the tick
63
+ * pass `swapPlane` to applyEffective, so a candidate that moved only hot fields (pricing values / quota weights /
64
+ * the `enabled:true` spelling, see catalogEntryFingerprint) or nothing on the plane swaps nothing. `prev ===
65
+ * undefined` (env-boot plane, no EffectiveConfig to compare) is conservatively "changed": the first center plane
66
+ * always swaps. Not gated on tier activity: a Runner's expanded catalog goes stale on ANY plane change,
67
+ * including a tiers-off flip, and the default entry rides the fingerprint because the Runner's copy carries its
68
+ * own `default` (codex R8; SAME resolver as applyEffective — resolveDefaultModelName, single source). */
62
69
  export declare function modelPlaneChanged(prev: EffectiveConfig | undefined, next: EffectiveConfig): boolean;
63
70
  //# sourceMappingURL=restart-signal.d.ts.map