@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
@@ -0,0 +1,78 @@
1
+ import { recordFailOpen } from "./observability/fail-open.js";
2
+ export const INSTRUCTION_SOURCE_CHANGED = "instructions.source_changed";
3
+ export function encodeInstructionSource(source) {
4
+ return source ?? "";
5
+ }
6
+ export function sameInstructionSourceBaseline(a, b) {
7
+ if (!a.recorded || !b.recorded)
8
+ return a.recorded === b.recorded;
9
+ return a.source === b.source;
10
+ }
11
+ export function decodeInstructionSource(raw) {
12
+ if (raw === null || raw === undefined)
13
+ return { recorded: false };
14
+ if (typeof raw !== "string")
15
+ throw new TypeError(`instruction_source holds a ${typeof raw}, not a string`);
16
+ return { recorded: true, source: raw === "" ? null : raw };
17
+ }
18
+ export function instructionSourceStoreOf(sessionStore) {
19
+ const s = sessionStore;
20
+ if (typeof s?.readInstructionSource !== "function" || typeof s.recordInstructionSource !== "function")
21
+ return undefined;
22
+ return { readInstructionSource: s.readInstructionSource.bind(s), recordInstructionSource: s.recordInstructionSource.bind(s) };
23
+ }
24
+ export function createInstructionSourceSeat(sessionStore, emit) {
25
+ const store = instructionSourceStoreOf(sessionStore);
26
+ return store && emit ? { store, emit } : undefined;
27
+ }
28
+ const describeSource = (source) => source ?? "no project instruction file";
29
+ export const BASELINE_IO_DEADLINE_MS = 2_000;
30
+ class BaselineDeadlineExceeded extends Error {
31
+ }
32
+ function withinDeadline(op) {
33
+ const running = Promise.resolve().then(op);
34
+ running.catch(() => undefined);
35
+ let timer;
36
+ const deadline = new Promise((_, reject) => {
37
+ timer = setTimeout(() => reject(new BaselineDeadlineExceeded(`no answer within ${BASELINE_IO_DEADLINE_MS}ms`)), BASELINE_IO_DEADLINE_MS);
38
+ timer.unref?.();
39
+ });
40
+ return Promise.race([running, deadline]).finally(() => clearTimeout(timer));
41
+ }
42
+ export async function announceInstructionSource(seat, sessionId, current) {
43
+ let baseline;
44
+ try {
45
+ baseline = await withinDeadline(() => seat.store.readInstructionSource(sessionId));
46
+ }
47
+ catch (e) {
48
+ degraded("read", sessionId, e);
49
+ return;
50
+ }
51
+ if (baseline.recorded && baseline.source === current)
52
+ return;
53
+ if (baseline.recorded) {
54
+ const previous = baseline.source;
55
+ try {
56
+ seat.emit({
57
+ code: INSTRUCTION_SOURCE_CHANGED,
58
+ message: `Project instructions for this run come from ${describeSource(current)}; the previous run of this session read ${describeSource(previous)}.`,
59
+ detail: { previous, current, sessionId },
60
+ sessionId,
61
+ });
62
+ }
63
+ catch (e) {
64
+ degraded("emit", sessionId, e);
65
+ }
66
+ }
67
+ try {
68
+ await withinDeadline(() => seat.store.recordInstructionSource(sessionId, baseline, current));
69
+ }
70
+ catch (e) {
71
+ degraded("write", sessionId, e);
72
+ }
73
+ }
74
+ function degraded(op, sessionId, e) {
75
+ const why = e instanceof BaselineDeadlineExceeded ? `timeout=${BASELINE_IO_DEADLINE_MS}ms` : `err=${e instanceof Error ? e.message : String(e)}`;
76
+ recordFailOpen("server.instruction-source.announce-degraded", `op=${op} session=${sessionId} ${why}`);
77
+ }
78
+ //# sourceMappingURL=instruction-source-notice.js.map
@@ -3,6 +3,15 @@ import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
3
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
5
5
  import type { LeaderRequestBody } from "./endpoint.js";
6
+ /** leader worker 的 durable 审批座(`LeaderWireConfig.durable` 函数席每次产出的一份)。 */
7
+ export interface LeaderDurableSeat {
8
+ readonly checkpointStore: CheckpointStoreFull;
9
+ readonly requireApproval: string[];
10
+ readonly deny?: string[];
11
+ readonly ttlMs?: number;
12
+ readonly autoBudget?: number;
13
+ readonly neverAuto?: string[];
14
+ }
6
15
  export interface LeaderWireConfig {
7
16
  /** Static-env compat mode (E2B only): the leader provisions/owns each env. Required when `envFactory` unset. */
8
17
  e2bApiKey?: string;
@@ -10,18 +19,17 @@ export interface LeaderWireConfig {
10
19
  * REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory.
11
20
  * Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */
12
21
  envFactory?: ExecutionEnvFactory;
13
- /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when set, each worker task carries the durable
14
- * question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker (checkpoint +
15
- * paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's per-task
16
- * durable wiring; ctx is seeded at provision so the surfaced token is resumable on the single-agent path. */
17
- durable?: {
18
- checkpointStore: CheckpointStoreFull;
19
- requireApproval: string[];
20
- deny?: string[];
21
- ttlMs?: number;
22
- autoBudget?: number;
23
- neverAuto?: string[];
24
- };
22
+ /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when the seat yields a value, that worker task carries
23
+ * the durable question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker
24
+ * (checkpoint + paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's
25
+ * per-task durable wiring; ctx is seeded at provision so the surfaced token is resumable on the single-agent path.
26
+ *
27
+ * 🔴 **函数席,不是快照**(S-668 波 2 ①;先例 = 下方 `deploymentPosture` 的 S-174 函数席):审批名单
28
+ * (`governance.approvalRequire`)自本批起在进程内会被中心热换。boot 期取一次值会把这条腿钉在起服那一刻的名单上
29
+ * —— 起服时名单为空 ⇒ 整块缺席、之后加进来的工具在 leader worker 上**永远**不被门住(fail-OPEN)。
30
+ * `provisionWorker` 每只 worker 调一次(同一只 worker 的 toolPolicy / resource 停车 / ctx 保温读同一份);
31
+ * 返回 `undefined` = 这只 worker 不接 durable 审批(与修前「键缺席」同义)。 */
32
+ durable?: () => LeaderDurableSeat | undefined;
25
33
  /** Durable tool-result offload store (TiDB). REQUIRED for `durable` to actually suspend: core's suspend
26
34
  * pre-commit refuses with the default InMemoryToolResultStore (offloaded results would deref to null after
27
35
  * a cross-replica resume) and silently falls back to the synchronous onAsk gate — which the leader does not
@@ -241,9 +241,10 @@ export function createLeaderRunner(cfg) {
241
241
  };
242
242
  const provisionWorker = async (sub) => {
243
243
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
244
+ const durable = cfg.durable?.();
244
245
  let durableSpec = {};
245
- if (cfg.durable) {
246
- const d = cfg.durable;
246
+ if (durable) {
247
+ const d = durable;
247
248
  durableSpec = {
248
249
  toolPolicy: combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({ requireApproval: d.requireApproval, ...(d.deny ? { deny: d.deny } : {}), ...(d.autoBudget ? { autoBudget: d.autoBudget } : {}), ...(d.neverAuto ? { neverAuto: d.neverAuto } : {}) })),
249
250
  checkpointStore: d.checkpointStore,
@@ -259,9 +260,9 @@ export function createLeaderRunner(cfg) {
259
260
  if (repairLoopActive && resourceCfg) {
260
261
  cfg.logger?.warn?.("leader_resource_suspend_disabled_for_repair_loop", { workerId: sub.workerId, reason: "LEADER_REPAIR_LOOP active — resource-suspend mutually exclusive with the repair loop (§10.5)" });
261
262
  }
262
- if (resourceCfg && !repairLoopActive && cfg.durable && cfg.toolResultStore && cfg.sessionStore) {
263
+ if (resourceCfg && !repairLoopActive && durable && cfg.toolResultStore && cfg.sessionStore) {
263
264
  resourceSpec = {
264
- checkpointStore: cfg.durable.checkpointStore,
265
+ checkpointStore: durable.checkpointStore,
265
266
  resourceSuspend: { scope: `_leader-resource-${sub.workerId}`, totalBudgetUsd: resourceCfg.totalBudgetUsd },
266
267
  limits: { ...workerLimits.limits, maxCostUsd: resourceCfg.sliceMaxCostUsd },
267
268
  maxSuspends: resourceCfg.maxSuspends,
@@ -270,7 +271,7 @@ export function createLeaderRunner(cfg) {
270
271
  else if (resourceCfg && !repairLoopActive) {
271
272
  cfg.logger?.warn?.("leader_resource_suspend_inactive", {
272
273
  workerId: sub.workerId,
273
- reason: !cfg.durable
274
+ reason: !durable
274
275
  ? "no durable checkpoint store"
275
276
  : !cfg.toolResultStore
276
277
  ? "no durable tool-result store"
@@ -278,7 +279,7 @@ export function createLeaderRunner(cfg) {
278
279
  });
279
280
  }
280
281
  const keepCtxWarm = (runner) => {
281
- const d = cfg.durable;
282
+ const d = durable;
282
283
  if (!d)
283
284
  return runner;
284
285
  const orig = runner.runTaskStream.bind(runner);
@@ -0,0 +1,14 @@
1
+ import type { EngineNotice } from "@sema-agent/core";
2
+ import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
3
+ export declare const MEMORY_LAYER_LOCKED_LEGACY: ServerNoticeCode;
4
+ /** 探测读数:锁住的层名(排序)+ 看不全的目录条数。挂载面不存在(新数据根)⇒ 两者皆空。 */
5
+ export interface LockedLegacyLayerProbe {
6
+ readonly layers: readonly string[];
7
+ readonly unreadable: number;
8
+ }
9
+ export declare function probeLockedLegacyMemoryLayers(memoryDir: string): LockedLegacyLayerProbe;
10
+ /** 锁住的层 ⇒ 一条通告(纯数据);没有 ⇒ `undefined`。detail 两键:`layers`(层名)与 `remedy`(同一句 `chmod -R u+w <memoryDir>`)。 */
11
+ export declare function buildMemoryLayerLockedLegacyNotice(memoryDir: string, layers: readonly string[]): EngineNotice | undefined;
12
+ /** boot 期的唯一调用口:探测一次、有锁住的层就经 `emit` 发一条;看不全的子树走 F 类留痕。不抛(不拒启)。 */
13
+ export declare function announceLockedLegacyMemoryLayers(memoryDir: string, emit: (notice: EngineNotice) => void): void;
14
+ //# sourceMappingURL=memory-layer-legacy-lock.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { lstatSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { recordFailOpen } from "./observability/fail-open.js";
4
+ export const MEMORY_LAYER_LOCKED_LEGACY = "memory.layer_locked_legacy";
5
+ const OWNER_WRITE = 0o200;
6
+ function shellQuoted(s) {
7
+ return `'${s.replace(/'/g, "'\\''")}'`;
8
+ }
9
+ const isMissing = (err) => err?.code === "ENOENT";
10
+ function treeHasLockedDir(dir, gaps) {
11
+ let st;
12
+ try {
13
+ st = lstatSync(dir);
14
+ }
15
+ catch (err) {
16
+ if (!isMissing(err))
17
+ gaps.n += 1;
18
+ return false;
19
+ }
20
+ if (!st.isDirectory())
21
+ return false;
22
+ if ((st.mode & OWNER_WRITE) === 0)
23
+ return true;
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ }
28
+ catch (err) {
29
+ if (!isMissing(err))
30
+ gaps.n += 1;
31
+ return false;
32
+ }
33
+ for (const e of entries) {
34
+ if (e.isDirectory() && treeHasLockedDir(join(dir, e.name), gaps))
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ export function probeLockedLegacyMemoryLayers(memoryDir) {
40
+ const gaps = { n: 0 };
41
+ let top;
42
+ try {
43
+ top = readdirSync(memoryDir, { withFileTypes: true });
44
+ }
45
+ catch (err) {
46
+ return { layers: [], unreadable: isMissing(err) ? 0 : 1 };
47
+ }
48
+ const layers = top.filter((e) => e.isDirectory() && treeHasLockedDir(join(memoryDir, e.name), gaps)).map((e) => e.name);
49
+ return { layers: layers.sort(), unreadable: gaps.n };
50
+ }
51
+ export function buildMemoryLayerLockedLegacyNotice(memoryDir, layers) {
52
+ if (layers.length === 0)
53
+ return undefined;
54
+ const remedy = `chmod -R u+w ${shellQuoted(memoryDir)}`;
55
+ const one = layers.length === 1;
56
+ return {
57
+ code: MEMORY_LAYER_LOCKED_LEGACY,
58
+ message: `memory layer${one ? "" : "s"} ${layers.map((l) => JSON.stringify(l)).join(", ")} ${one ? "still carries" : "still carry"} the read-only permission bits ` +
59
+ "an engine from 7.30.x or earlier (a server replica at 7.100.0 or earlier) sets on every layer it mounts read-only — deleting an " +
60
+ "entry there (memory erase / origin clear) fails for a server that does not run as root: the request answers 500, its journal " +
61
+ `stays on disk and this replica's next start fails. Run \`${remedy}\` once no such engine still runs on this store (an older ` +
62
+ "replica sets the bits again whenever it mounts a layer read-only). Starting anyway.",
63
+ detail: { layers: [...layers], remedy },
64
+ };
65
+ }
66
+ export function announceLockedLegacyMemoryLayers(memoryDir, emit) {
67
+ const probe = probeLockedLegacyMemoryLayers(memoryDir);
68
+ if (probe.unreadable > 0)
69
+ recordFailOpen("server.memory.locked-layer-probe-partial", `unreadable=${probe.unreadable}`);
70
+ const notice = buildMemoryLayerLockedLegacyNotice(memoryDir, probe.layers);
71
+ if (notice !== undefined)
72
+ emit(notice);
73
+ }
74
+ //# sourceMappingURL=memory-layer-legacy-lock.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * core 7.31.1 #1094 —— **`memory.target_outside_store`** 在本仓的唯一读法属主(纯叶:零 http / 零 boot 依赖)。
3
+ *
4
+ * ## 这个码说的是什么(core `docs/wire-consumption/7.31.1.md` 逐 face 表,tag v7.31.1 亲读)
5
+ * 记忆 store 里的一个目录经软链通到了 store **外**(或 store 根在 backend 打开 / 引擎首次挂载之后被改指)⇒ 引擎对落点在外的
6
+ * 删 / 写 / 重放 / 自身写一律**不做**,抛普通 `Error` + `.code`(文案以码领头,点名动作、路径、解析到哪)。core 的读法原话:
7
+ * operator 要处置的拒(去掉那只目录链),**不是**可重试的 IO 故障,也**不是**对调用方的裁决;链去掉之前重发恒拒。
8
+ *
9
+ * ## 两条到达车道,两个读者(本模块各给一只,别处不许再写第二份判据)
10
+ * ① **抛出面**(抹除 / 出处 / 外源列表 / 清标 / bundle 导出导入 —— 凡触 store 树的 backend 面):`.code` 就是码
11
+ * ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,一处字面量)。
12
+ * ② **报告面**(harvest `incident`):core 契约写明 report 车道上码**只在文本里** —— `sidecar_corrupt` 的 detail 恰两种形
13
+ * `memory commit refused: memory.target_outside_store: …` / `memory control plane refused: memory.target_outside_store: …`
14
+ * (core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字)⇒ {@link targetOutsideStoreInIncident} 只认**这两个 lead 紧跟码**
15
+ * 的位置:lead 与码都是 core 铸的字面,路径与引用数据全在码之后。🔴 不做全文子串找(codex 一审 C2:一条真控制面损坏若在
16
+ * 它引用的坏数据里恰好带着这串字,子串找会把它标成目录链 —— 两者运维动作相反)。不读散文、不猜语义;结构化码位
17
+ * (`HarvestReport.incident` / `.warnings` 带码)是向 core 索取的欠账。
18
+ * ⚠️ 同一拒的**还原类**(缺失文件还原 / 只读层还原 / `MEMORY.md` 写)只进 `HarvestReport.warnings`(可以 `ok:true`、无 incident),
19
+ * 那几行的前缀是**条目路径**(模型可写)—— 本仓今天不读 warnings 车道(既有面,不是本批回归),也不在模型可写的文本上
20
+ * 找码;这一半候 core 的结构化码位(登记在案,codex 一审 C3)。
21
+ *
22
+ * ## 不是这个码的三形(core 同表「different surface」,本仓**不**自拼码,如实走各自既有臂)
23
+ * · journal 重放目标目录成链 ⇒ `ControlPlaneCorruptError`(无 `.code`,KL-1094-b)⇒ 路由 typed 500 `internal.memory_control_plane_corrupt`;
24
+ * · 引擎原地写撞硬链(nlink>1)⇒ 文本 `has N names (a hard link)`、无码(KL-1094-g / #1105)⇒ 走该写点既有的失败车道(harvest
25
+ * warnings / 挂载失败),抛出面够不着;
26
+ * · 解析故障(`EACCES` / `ELOOP` / `ENOTDIR`)⇒ 以 errno 本身抛 ⇒ 路由未分类 500 + 运维 warn(`String(err)` 带 core 补救句)。
27
+ */
28
+ /** core 的码(`src/core/memory-engine/layout.ts` `TARGET_OUTSIDE_STORE`;core 不在包根导出它,本常量是它在本仓的唯一写点)。 */
29
+ export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
30
+ /** 抛出面:`err` 是这一拒 ⇒ 原样交回(已窄化成 `Error`,调用方要它的 `message`);否则 `undefined`。形 = core 的 `Error` + `.code`。 */
31
+ export declare function targetOutsideStoreOf(err: unknown): Error | undefined;
32
+ /** 报告面:harvest `incident` 是这一拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成链)。 */
33
+ export declare function targetOutsideStoreInIncident(incident: {
34
+ readonly kind: string;
35
+ readonly detail: string;
36
+ }): typeof TARGET_OUTSIDE_STORE | undefined;
37
+ //# sourceMappingURL=memory-store-refusal.d.ts.map
@@ -0,0 +1,14 @@
1
+ export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
2
+ export function targetOutsideStoreOf(err) {
3
+ if (!(err instanceof Error))
4
+ return undefined;
5
+ const { code } = err;
6
+ return code === TARGET_OUTSIDE_STORE ? err : undefined;
7
+ }
8
+ const INCIDENT_PREFIXES = [`memory commit refused: ${TARGET_OUTSIDE_STORE}: `, `memory control plane refused: ${TARGET_OUTSIDE_STORE}: `];
9
+ export function targetOutsideStoreInIncident(incident) {
10
+ if (incident.kind !== "sidecar_corrupt")
11
+ return undefined;
12
+ return INCIDENT_PREFIXES.some((p) => incident.detail.startsWith(p)) ? TARGET_OUTSIDE_STORE : undefined;
13
+ }
14
+ //# sourceMappingURL=memory-store-refusal.js.map
@@ -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` 条(表的「不许有表外不变量」职责);