@sema-agent/server 7.45.0 → 7.46.0-rc.2

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 (41) hide show
  1. package/USAGE.md +38 -0
  2. package/dist/approval-card.d.ts +106 -40
  3. package/dist/approval-card.js +45 -8
  4. package/dist/boot/coordinators.d.ts +1 -1
  5. package/dist/boot/memory-consolidation.d.ts +191 -0
  6. package/dist/boot/memory-consolidation.js +132 -0
  7. package/dist/boot/runner-deps.d.ts +1 -1
  8. package/dist/config-types.d.ts +48 -2
  9. package/dist/config-types.js +1 -0
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.js +53 -3
  12. package/dist/http/routes/capabilities.js +4 -0
  13. package/dist/http/routes/memory-compliance.d.ts +102 -0
  14. package/dist/http/routes/memory-compliance.js +113 -0
  15. package/dist/http/routes/memory-consolidation.d.ts +60 -0
  16. package/dist/http/routes/memory-consolidation.js +155 -0
  17. package/dist/http/routes/memory-origin.d.ts +124 -0
  18. package/dist/http/routes/memory-origin.js +193 -0
  19. package/dist/http/routes/rules.js +1 -1
  20. package/dist/http/routes/side-query.js +1 -0
  21. package/dist/http/server.d.ts +71 -2
  22. package/dist/http/server.js +27 -2
  23. package/dist/main.js +55 -2
  24. package/dist/memory-operator-faces.d.ts +211 -0
  25. package/dist/memory-operator-faces.js +76 -0
  26. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  27. package/dist/plugins/checkpoint-store-sql.js +31 -17
  28. package/dist/plugins/local-checkpoint-store.js +3 -3
  29. package/dist/plugins/permission-rule-store-file.d.ts +2 -2
  30. package/dist/plugins/permission-rule-store-file.js +41 -4
  31. package/dist/plugins/permission-rule-store-sql.d.ts +2 -2
  32. package/dist/plugins/permission-rule-store-sql.js +59 -18
  33. package/dist/plugins/store-backend.d.ts +1 -1
  34. package/dist/plugins/tidb-pool.js +3 -3
  35. package/dist/rules-consent.d.ts +30 -3
  36. package/dist/rules-consent.js +50 -7
  37. package/dist/task-cwd.d.ts +1 -1
  38. package/dist/tool-approval.d.ts +43 -10
  39. package/dist/tool-approval.js +105 -27
  40. package/dist/trace/core-keyset-guard.d.ts +2 -2
  41. package/package.json +3 -3
@@ -0,0 +1,132 @@
1
+ import { redactSecrets } from "../trace/redact.js";
2
+ import { expandTiers, MemoryEngine, readConsolidationDriverRun, resolveMemoryConsolidationDriver, runMemoryConsolidationDriver, } from "@sema-agent/core";
3
+ export function resolveConsolidationDriverSeat(deps) {
4
+ let options;
5
+ try {
6
+ options = resolveMemoryConsolidationDriver(deps);
7
+ }
8
+ catch (err) {
9
+ throw new Error(`MEMORY_CONSOLIDATION_DRIVER=on but this deployment has no usable consolidation model seat — refusing to start. ` +
10
+ `A whole-library consolidation reads ~1e5 prompt tokens per cycle, so core deliberately REFUSES rather than ` +
11
+ `falling back to the main model; folding that refusal back to "valve off" here would leave an operator who ` +
12
+ `explicitly asked for consolidation with a library that silently never folds. Fix one of: declare ` +
13
+ `roles.consolidate (or roles.summarize / a "flash" tier binding), point the driver at an explicit chat seat, ` +
14
+ `or set MEMORY_CONSOLIDATION_DRIVER=off. Upstream said: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
15
+ }
16
+ return { options, seat: seatLabel(deps), model: options.model };
17
+ }
18
+ function seatLabel(deps) {
19
+ if (deps.memoryConsolidationDriver?.chat !== undefined)
20
+ return "explicit";
21
+ try {
22
+ const expanded = expandTiers(deps.models, deps.tiers);
23
+ const catalogOnly = expanded === undefined ? undefined : Object.fromEntries(Object.entries(expanded));
24
+ resolveMemoryConsolidationDriver({ ...deps, models: catalogOnly, tiers: undefined, roles: { consolidate: deps.roles?.consolidate } });
25
+ return "role:consolidate";
26
+ }
27
+ catch {
28
+ return "role:summarize";
29
+ }
30
+ }
31
+ export function createPerScopeSingleFlight() {
32
+ const inFlight = new Map();
33
+ return (scope, run) => {
34
+ const live = inFlight.get(scope);
35
+ if (live !== undefined)
36
+ return live;
37
+ const started = Promise.resolve().then(run).finally(() => {
38
+ if (inFlight.get(scope) === started)
39
+ inFlight.delete(scope);
40
+ });
41
+ inFlight.set(scope, started);
42
+ return started;
43
+ };
44
+ }
45
+ export function assertConsolidationDriverWirable(input) {
46
+ if (!input.enabled)
47
+ return;
48
+ if (!input.memoryEngineWired) {
49
+ throw new Error(`MEMORY_CONSOLIDATION_DRIVER=on arms the memory-consolidation valve, but this deployment wires NO memory engine ` +
50
+ `— there is no library to fold, and the two admin endpoints would answer over an engine that does not exist. ` +
51
+ `(The engine is wired only on single-user deployments with MEMORY_ENGINE on; a multi-tenant worker keeps memory ` +
52
+ `dark by design.) Wire the memory engine, or set MEMORY_CONSOLIDATION_DRIVER=off.`);
53
+ }
54
+ if (!input.controlPlaneOwned) {
55
+ throw new Error(`MEMORY_CONSOLIDATION_DRIVER=on arms the memory-consolidation valve, but the wired memory backend ` +
56
+ `(MEMORY_ENGINE_BACKEND=${input.memoryEngineBackend}) does NOT own an engine control plane (controlPlaneRoot) — ` +
57
+ `core would derive one on THIS replica's local disk, and a consolidation run's resumable account would die with ` +
58
+ `the pod (a lost run re-mints the whole library at full model cost on the next attempt). The file memory backend ` +
59
+ `owns a control plane; the pg/tidb memory backends do not. Set MEMORY_ENGINE_BACKEND=file, or set ` +
60
+ `MEMORY_CONSOLIDATION_DRIVER=off.`);
61
+ }
62
+ if (input.provenance === "off") {
63
+ throw new Error(`MEMORY_CONSOLIDATION_DRIVER=on is incompatible with MEMORY_PROVENANCE=off — core refuses to enable consolidation ` +
64
+ `under provenance "off" because the fold law must be able to MINT origin markers (a marked input's product would ` +
65
+ `otherwise commit unmarked, i.e. the fold would launder provenance). Set MEMORY_PROVENANCE=carry (or unset it, ` +
66
+ `which is the same posture), or set MEMORY_CONSOLIDATION_DRIVER=off.`);
67
+ }
68
+ }
69
+ export function buildConsolidationValveAudit(input) {
70
+ if (!input.enabled)
71
+ return { warnings: [] };
72
+ const warnings = [];
73
+ if (input.operatorPrincipals.length === 0) {
74
+ warnings.push({
75
+ tag: "memory_consolidation_valve_unreachable",
76
+ detail: "MEMORY_CONSOLIDATION_DRIVER=on armed the consolidation valve and both admin endpoints are mounted, but " +
77
+ "OPERATOR_PRINCIPALS is EMPTY — an empty operator list means NO ONE is an operator (it deliberately does " +
78
+ "not mean 'everyone'), so POST /v1/admin/memory/consolidation/run and GET /v1/admin/memory/consolidation " +
79
+ "answer 403 auth.operator_only to every caller and capabilities.memoryConsolidationDriver reports false. " +
80
+ "Set OPERATOR_PRINCIPALS to the principal(s) allowed to drive consolidation, and send that principal in " +
81
+ "the configured principal header. (Not a startup refusal: OPERATOR_PRINCIPALS is shared by every operator " +
82
+ "endpoint on this worker, so one valve must not decide whether the whole worker starts.)",
83
+ });
84
+ }
85
+ if (!input.hasServiceAuth && !input.allowUnauthedWrites) {
86
+ warnings.push({
87
+ tag: "memory_consolidation_run_credential_gated",
88
+ detail: "MEMORY_CONSOLIDATION_DRIVER=on armed the consolidation valve, but this worker has NO service credential " +
89
+ "(SERVICE_AUTH_TOKEN / per-system tokens are unset) and ALLOW_UNAUTHED_WRITES is not on. POST " +
90
+ "/v1/admin/memory/consolidation/run runs the model, so it rides the billable-submit gate and is refused " +
91
+ "with 503 auth.service_token_required BEFORE it reaches the route — while capabilities." +
92
+ "memoryConsolidationDriver still reports true (a family-wide gap this worker shares with adoption / memory " +
93
+ "bundle / memory compliance; it is documented, not silently accepted). GET /v1/admin/memory/consolidation " +
94
+ "is unaffected (it runs no model). Set SERVICE_AUTH_TOKEN, or set ALLOW_UNAUTHED_WRITES=true for local " +
95
+ "development only.",
96
+ });
97
+ }
98
+ return { warnings };
99
+ }
100
+ const STOP_DETAIL_LOG_MAX = 600;
101
+ export function buildStopDetailAudit(input) {
102
+ return {
103
+ scope: input.scope,
104
+ runId: input.runId,
105
+ outcome: input.outcome,
106
+ untrustedDetail: redactSecrets(input.stopDetail).slice(0, STOP_DETAIL_LOG_MAX),
107
+ };
108
+ }
109
+ export function createMemoryConsolidationFaces(store, input) {
110
+ const engine = new MemoryEngine({
111
+ backend: store.backend,
112
+ memoryDir: store.root,
113
+ consolidation: {},
114
+ ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
115
+ ...(input.onIncident !== undefined ? { onIncident: input.onIncident } : {}),
116
+ });
117
+ const flight = createPerScopeSingleFlight();
118
+ return {
119
+ seat: input.seat.seat,
120
+ model: input.seat.model,
121
+ scopes: input.scopes,
122
+ run: (scope) => flight(scope, async () => {
123
+ const receipt = await runMemoryConsolidationDriver(engine, scope, input.seat.options);
124
+ if (receipt.stopDetail !== undefined) {
125
+ input.onStopDetail?.(buildStopDetailAudit({ scope, runId: receipt.runId, outcome: receipt.outcome, stopDetail: receipt.stopDetail }));
126
+ }
127
+ return receipt;
128
+ }),
129
+ lastRun: (scope) => readConsolidationDriverRun(engine.controlPlaneRoot, scope),
130
+ };
131
+ }
132
+ //# sourceMappingURL=memory-consolidation.js.map
@@ -90,7 +90,7 @@ export interface RunnerDepsCtx {
90
90
  runtimeCapsResolver: RunnerDeps["runtimeCapsResolver"];
91
91
  fileSnapshotStore: ReturnType<StoreBackend["fileSnapshot"]> | undefined;
92
92
  /** #154 车二:持久化权限规则店 provider(core `RunnerDeps.permissionRuleStore`)。缺席 ⇒ 引擎的
93
- * `permissionRules.storeWired` 如实报 false、`AskRequest.ruleSuggestions` 不铸(诚实缺席)。 */
93
+ * `permissionRules.storeWired` 如实报 false、`AskRequest.ruleOffers` 不铸(诚实缺席)。 */
94
94
  permissionRuleStore: RunnerDeps["permissionRuleStore"];
95
95
  /** #324(core 5.50 design/338):mid-turn MCP 撤销台账席 —— core 每次 MCP dispatch 前同步探
96
96
  * `isRevoked(serverName)`,被撤服务器的调用结算为 coded 拒绝 `mcp.server_revoked`(known-not-executed)。
@@ -44,6 +44,39 @@ export interface RetentionSweepConfig {
44
44
  /** sweep 节律(秒)。`0` = **关**(默认);租约 TTL = 2×本值。 */
45
45
  intervalSec: number;
46
46
  }
47
+ /**
48
+ * design/378 D2 —— 周期腿节律的**下界**(秒)。一轮 consolidation 读全库(~1e5 prompt tokens)且
49
+ * core 自带 24h 的时间闸,比这更密的节律只可能是把分钟/毫秒当成秒写的手滑。夹取(不是拒启)方向安全:
50
+ * 夹上去只会**少**跑,而这条腿花的是钱。
51
+ * ⚠️ 今天这条夹取**还够不着**:本 build 没有周期腿,任何正值在解析期就拒启(见
52
+ * {@link MemoryConsolidationDriverConfig.intervalSec})。常量在拒启文案里被单点引用,腿落地那批撤掉
53
+ * 那道拒之后它即刻生效 —— 留着是为了「那天不必有人再猜一个数」。
54
+ */
55
+ export declare const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
56
+ /**
57
+ * design/378 D1/D2 —— consolidation driver 阀门的三根旋钮(语义与三问见
58
+ * {@link ServiceConfigFlat.memoryConsolidationDriver})。纯数据。
59
+ */
60
+ export interface MemoryConsolidationDriverConfig {
61
+ /** 阀门(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,缺省 `off`)。`false` = 零装配零探针:
62
+ * 座不解析、引擎不构造、两个 admin 口整个不挂(404 族,**不是** 501)、能力位翻假。 */
63
+ enabled: boolean;
64
+ /**
65
+ * 周期腿节律(秒;env `MEMORY_CONSOLIDATION_INTERVAL_SEC`,缺省 `0` = 不周期跑)。
66
+ * 🔴 **本 build 里任何正值都拒启**:结构上没有周期腿(没有可用的真互斥基建 —— 全文与偏离登记见
67
+ * `boot/memory-consolidation.ts` 顶注与 designs/378 §遗留 遗-1)。所以本字段今天**恒为 0**;它留在
68
+ * 类型上是因为拒启文案与周期腿落地那一批共用同一个字段名与同一条下界语义
69
+ * ({@link MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC})。
70
+ */
71
+ intervalSec: number;
72
+ /** 阀门的 scope 表(env `MEMORY_CONSOLIDATION_SCOPES`,逗号表;缺省空表)。v1 的两个用途:
73
+ * ①恰好一条时 = 两个 admin 口省略 `scope` 的**唯一**缺省(两条及以上恒不猜——在两个库之间猜一个
74
+ * 去花钱是本设计要消灭的形);②周期腿落地时的 scope 源(设计稿 D2)。
75
+ * 🔴 **不是授权边界**:本仓今天没有任何授权源能对一个 operator 收窄 scope(理由与亲读依据逐字见
76
+ * `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注),在本层拿它当白名单就是**发明
77
+ * 第二套授权语义**。显式传的 scope 照跑,表只管缺省。 */
78
+ scopes: readonly string[];
79
+ }
47
80
  /** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
48
81
  * scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
49
82
  export interface ScopedMcpServer {
@@ -449,6 +482,19 @@ export interface ServiceConfigFlat {
449
482
  * ⚠️ **不冻进 checkpoint**:resume 的那条腿跟**当前**部署配置走(core 成文)。
450
483
  * 运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.provenance`(`null`=本部署未设)。 */
451
484
  memoryProvenance?: "off" | "carry";
485
+ /**
486
+ * design/378(core 5.58 design/376 片②)—— 记忆 **consolidation driver 阀门**的三根旋钮。
487
+ * **恒在场**(不是可选块,与 {@link retentionSweep} 同姿态):三根都有文档化的缺省,而缺省本身
488
+ * 是一条要能被读出来的姿态 —— `{enabled:false, intervalSec:0, scopes:[]}` = 「阀门关着」。
489
+ *
490
+ * 三问(设计稿 §消长账):谁需要 = 记忆库只涨不折的 TOB 部署(检索质量随库龄衰减);谁被伤 =
491
+ * 误开 + 错 role 的部署(一轮 consolidation 读全库 ~1e5 prompt tokens,静默升级到最贵 default
492
+ * 就是把一次维护跑成一笔意外账单);补偿 = 阀门默认关 + 座解析失败拒启 + D3 投影把每轮花了什么
493
+ * 摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。
494
+ *
495
+ * 语义与解析见 {@link MemoryConsolidationDriverConfig} 与 config.ts 的 `parseMemoryDomain`。
496
+ */
497
+ memoryConsolidationDriver: MemoryConsolidationDriverConfig;
452
498
  /** debt #322-B(core 5.48 C18 提货件):委派入口 caps 的部署旋钮 —— env `DELEGATION_MAX_CONCURRENT` /
453
499
  * `DELEGATION_MAX_PER_SESSION`(整数,numEnvBounded 响亮拒坏值)。**缺席 = 不铸键**(core 自缺省
454
500
  * CC parity 20/200;`memoryDelegationEvidence` 同姿态);两键各自独立可设,pair 合法性
@@ -1188,7 +1234,7 @@ export interface ServiceConfigFlat {
1188
1234
  * 判据见 `boot/permission-rules-audit.ts`。
1189
1235
  *
1190
1236
  * 关 ⇒ 规则店根本不装配:`RunnerDeps.permissionRuleStore` 缺席(引擎的 `permissionRules.storeWired`
1191
- * 如实报 false)、ask 帧不带 `ruleSuggestions`、回决的 `persistRule` 拒、`/v1/rules/*` 全族 501。
1237
+ * 如实报 false)、ask 帧不带 `ruleOffers`、回决的 `persistRule` 拒、`/v1/rules/*` 全族 501。
1192
1238
  * 三张表仍随中央 `ensureSchema` 建(纯 CREATE、零行),这不是行为面。
1193
1239
  */
1194
1240
  permissionRulesEnabled: boolean;
@@ -1407,7 +1453,7 @@ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" |
1407
1453
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1408
1454
  export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
1409
1455
  /** 组:memory(记忆面 + TOC 同步腿)。 */
1410
- export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1456
+ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryConsolidationDriver" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
1411
1457
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
1412
1458
  * 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。
1413
1459
  * design/170 的三件部署治理声明(`compliancePosture`/`lockedConfigKeys`/`retentionPolicy`)同归本组:
@@ -1,2 +1,3 @@
1
1
  export const RETENTION_MODES = ["audit-only", "enforce"];
2
+ export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
2
3
  //# sourceMappingURL=config-types.js.map
package/dist/config.d.ts CHANGED
@@ -5,8 +5,8 @@ import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./co
5
5
  * every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
6
6
  * importing config-types.js directly — a type-only leaf, no loader baggage). */
7
7
  export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat, ServiceConfigGroups, ServiceStoreConfig, ServiceModelPlaneConfig, ServiceApprovalConfig, ServiceMemoryConfig, ServiceAuthConfig, ServiceOrchestrationConfig, ServiceLimitsHttpConfig, ServiceObservabilityConfig, ServiceIntegrationsConfig, MemoryEmbedderConfig, // #228:embedder 坐标(plugins/memory-embedder.ts 消费同一属主类型)
8
- A2aServeConfig, A2aServeSkill, RetentionMode, RetentionSweepConfig, } from "./config-types.js";
9
- export { RETENTION_MODES } from "./config-types.js";
8
+ A2aServeConfig, A2aServeSkill, RetentionMode, RetentionSweepConfig, MemoryConsolidationDriverConfig, } from "./config-types.js";
9
+ export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
10
10
  /** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
11
11
  * tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
12
12
  * must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
package/dist/config.js CHANGED
@@ -9,9 +9,9 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
9
9
  import { isV2ScopeKey } from "./memory-scope.js";
10
10
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
11
11
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
12
- import { RETENTION_MODES } from "./config-types.js";
12
+ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
13
13
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
14
- export { RETENTION_MODES } from "./config-types.js";
14
+ export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
15
15
  function csv(name) {
16
16
  return (process.env[name] ?? "")
17
17
  .split(",")
@@ -983,6 +983,55 @@ function parseMemoryEmbedder(ctx) {
983
983
  ...(apiKey !== undefined ? { apiKey } : {}),
984
984
  };
985
985
  }
986
+ function parseMemoryConsolidationDriver() {
987
+ const requireNonBlank = (name, raw) => {
988
+ if (raw === undefined)
989
+ return undefined;
990
+ const trimmed = raw.trim();
991
+ if (trimmed === "" || trimmed.split(",").every((t) => t.trim() === "")) {
992
+ throw new Error(`env ${name} is set but empty (or has no parseable value) — a memory-consolidation knob that arms nothing must not boot silently. ` +
993
+ `UNSET ${name} to take the documented default, or give it a real value.`);
994
+ }
995
+ return trimmed;
996
+ };
997
+ const word = requireNonBlank("MEMORY_CONSOLIDATION_DRIVER", process.env.MEMORY_CONSOLIDATION_DRIVER);
998
+ if (word !== undefined && word !== "on" && word !== "off") {
999
+ throw new Error(`MEMORY_CONSOLIDATION_DRIVER must be exactly "on" or "off", got ${JSON.stringify(word)} — a misspelled ON word would run as OFF, ` +
1000
+ `and an OFF consolidation valve is INVISIBLE from the outside (the two admin endpoints are not mounted at all, so an operator ` +
1001
+ `who believes the valve is armed gets a 404 rather than a diagnosis). This knob is new in server 7.46 and has no legacy ` +
1002
+ `spellings to preserve, unlike MEMORY_ENGINE's two-family word table.`);
1003
+ }
1004
+ const enabled = word === "on";
1005
+ let intervalSec = 0;
1006
+ const intervalRaw = requireNonBlank("MEMORY_CONSOLIDATION_INTERVAL_SEC", process.env.MEMORY_CONSOLIDATION_INTERVAL_SEC);
1007
+ if (intervalRaw !== undefined) {
1008
+ const n = parseNumOrFailNonNegative("MEMORY_CONSOLIDATION_INTERVAL_SEC", intervalRaw);
1009
+ if (!Number.isInteger(n)) {
1010
+ throw new Error(`env MEMORY_CONSOLIDATION_INTERVAL_SEC=${n} must be a whole number of seconds (0 = no periodic leg)`);
1011
+ }
1012
+ if (n > 0) {
1013
+ throw new Error(`env MEMORY_CONSOLIDATION_INTERVAL_SEC=${n} asks for a periodic consolidation cadence, but this server build ` +
1014
+ `has no periodic consolidation leg — and it deliberately does not fake one: a fenced, single-executor tick ` +
1015
+ `needs a durable lease, while LEADER_ENABLED here is a plain boolean config gate (no election, no lease, no ` +
1016
+ `fencing — every replica that sets it self-declares leader), and a consolidation round that runs twice costs ` +
1017
+ `a second WHOLE-LIBRARY distillation. Refusing to boot rather than accepting a cadence nothing honors: a ` +
1018
+ `worker that looks healthy while its library never folds is exactly the failure this valve exists to prevent. ` +
1019
+ `Drive POST /v1/admin/memory/consolidation/run from an external scheduler (cron / k8s CronJob) at the cadence ` +
1020
+ `you want, and unset MEMORY_CONSOLIDATION_INTERVAL_SEC. (When the periodic leg lands, values below ` +
1021
+ `${MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC} seconds will clamp up to it — one round reads the whole library and ` +
1022
+ `core already holds a 24h time gate.)`);
1023
+ }
1024
+ intervalSec = n;
1025
+ }
1026
+ const scopesRaw = requireNonBlank("MEMORY_CONSOLIDATION_SCOPES", process.env.MEMORY_CONSOLIDATION_SCOPES);
1027
+ const scopes = scopesRaw !== undefined ? csv("MEMORY_CONSOLIDATION_SCOPES") : [];
1028
+ if (!enabled && (intervalSec > 0 || scopes.length > 0)) {
1029
+ throw new Error(`MEMORY_CONSOLIDATION_INTERVAL_SEC / MEMORY_CONSOLIDATION_SCOPES are configured but MEMORY_CONSOLIDATION_DRIVER is not "on" — ` +
1030
+ `the consolidation valve is OFF, so neither knob reaches anything (the two admin endpoints are not even mounted). ` +
1031
+ `Set MEMORY_CONSOLIDATION_DRIVER=on, or unset the other two.`);
1032
+ }
1033
+ return { enabled, intervalSec, scopes };
1034
+ }
986
1035
  function parseMemoryDomain(ctx) {
987
1036
  const { requirePrincipal } = ctx;
988
1037
  const memoryEngineEnabled = parseMemoryEngineWord(env("MEMORY_ENGINE", "on"));
@@ -1041,6 +1090,7 @@ function parseMemoryDomain(ctx) {
1041
1090
  ...(process.env.MEMORY_PROVENANCE !== undefined
1042
1091
  ? { memoryProvenance: enumEnv("MEMORY_PROVENANCE", "carry", ["off", "carry"]) }
1043
1092
  : {}),
1093
+ memoryConsolidationDriver: parseMemoryConsolidationDriver(),
1044
1094
  ...(process.env.DELEGATION_MAX_CONCURRENT !== undefined || process.env.DELEGATION_MAX_PER_SESSION !== undefined
1045
1095
  ? {
1046
1096
  delegationEntryCaps: {
@@ -1567,7 +1617,7 @@ const APPROVAL_GROUP_KEYS = [
1567
1617
  ];
1568
1618
  const MEMORY_GROUP_KEYS = [
1569
1619
  "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
1570
- "memoryPersistenceCapable", "memorySync", "memoryEmbedder", "memoryOrgAdmissionMode", "memoryDelegationEvidence", "memoryProvenance", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
1620
+ "memoryPersistenceCapable", "memorySync", "memoryEmbedder", "memoryOrgAdmissionMode", "memoryDelegationEvidence", "memoryProvenance", "memoryConsolidationDriver", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
1571
1621
  "projectMemoryEnabled", "syncImportLeaseStaleSec",
1572
1622
  ];
1573
1623
  const AUTH_GROUP_KEYS = [
@@ -66,10 +66,14 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
66
66
  sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
67
67
  adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
68
68
  memoryBundle: Boolean(deps.memoryBundleExport && deps.memoryBundleImport) && deps.config.operatorPrincipals.length > 0,
69
+ memoryCompliance: Boolean(deps.memoryCompliance) && deps.config.operatorPrincipals.length > 0,
70
+ memoryOrigin: Boolean(deps.memoryOriginFace) && deps.config.operatorPrincipals.length > 0,
71
+ memoryConsolidationDriver: Boolean(deps.memoryConsolidation) && deps.config.operatorPrincipals.length > 0,
69
72
  outcomeLedger: Boolean(deps.outcomeSink?.summary) && (deps.config.requirePrincipal !== true || deps.config.operatorPrincipals.length > 0),
70
73
  permissionRules: Boolean(deps.ruleConsent) && gatedPrincipal(req, deps.config) !== undefined,
71
74
  permissionRulesRevoke: Boolean(deps.ruleConsent),
72
75
  respondFreeFormRules: true,
76
+ respondBatchRuleOffers: true,
73
77
  oneShot: true,
74
78
  policy: true,
75
79
  usage: Boolean(deps.costQuota),
@@ -0,0 +1,102 @@
1
+ /**
2
+ * design/316 件③ —— **出处 / 抹除合规面的 operator 两口**(core 5.57.0 `MemoryEngine.provenanceOf` /
3
+ * `eraseMemoryEntries`,design/178 v2-a / v2-b)。
4
+ *
5
+ * · `GET /v1/memory/entries/:entryId/provenance` —— 回 `EntryProvenanceAccount` 原样;
6
+ * · `POST /v1/memory/erase` —— 请求 `{requestId, select, allowUnevidenced?}`,回
7
+ * `MemoryErasureAttestation` 原样。
8
+ *
9
+ * ── 为什么是 operator-only ─────────────────────────────────────────────────────────────────────
10
+ * 与 #264 的 bundle 两口同族(memory-bundle.ts 的同名段):
11
+ * · erase 是**治理写面**,而且是本族里爆炸半径最大的一口 —— 整 scope / 整会话删除是**合法**选择子,
12
+ * 而 core 的大规模删除熔丝对它**显式不适用**(engine.d.ts:684 逐字:那道熔丝守的是「文件在 harvest
13
+ * 时悄悄不见了」的事故形,一次显式授权的抹除靠在收执里逐 id 列举来自证);
14
+ * · provenance 是**跨租户治理读**:一条 entry 的绑定 / 贡献会话 / 污染标记 / 托管链事件全在里面,
15
+ * 按定义超出任何单个 principal 的自助边界。
16
+ * 缺 principal 的形照 sibling operator 端点(adoption / retention-ops / memory-bundle)401。
17
+ *
18
+ * ── 门序(逐字同 routes/memory-bundle.ts)─────────────────────────────────────────────────────
19
+ * 身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。**授权在能力之前**:一个够不着任何东西的调用方
20
+ * 不该从「这个部署有没有记忆引擎」上读出部署形态。
21
+ *
22
+ * ── 能力面的诚实形:为什么这两口的 501 判据与 bundle **不同** ─────────────────────────────────
23
+ * 两族同码 `capability.memory_engine_required`(消费端分支相同:换部署形态),但**判据不是同一条**:
24
+ * bundle 要的是后端的 v2-c 复合面(`exportSnapshotOf` / `importBundleCommit`),本族要的是后端**自带
25
+ * 控制面归属**(`controlPlaneRoot`)—— 这两口真的读引擎控制面(lineage / challenges / 托管链),而缺
26
+ * `controlPlaneRoot` 时 core 会把控制面落到**副本本地盘**上,与本仓 stateless replicas 正面冲突。
27
+ * 判在**挂载期**(`createMemoryComplianceFaces` 返回 undefined ⇒ 两口整个不挂),全文见
28
+ * `src/memory-operator-faces.ts` 头注。⚠️ 因此本族 501 的**文案**与 bundle 那句刻意不同字:两句指的是
29
+ * 不同的旋钮,复用一句会把运维指到一个根本没问题的地方去(bundle 那条自己的头注就吃过这个亏)。
30
+ *
31
+ * ── 透传的理由(照 memory-bundle.ts 的「披露的传导」段同源)───────────────────────────────────
32
+ * 两口的 200 体都**原样下发**,不投影、不复述、不补键。理由是单一属主 + 账形是**开放判别式**:
33
+ * · `EntryProvenanceAccount.binding` 是 `CommittedBinding | {state:"absent"} | {state:"unknown",…}`
34
+ * 的判别式,`custody.events` 是 core 的 `TransferEvidence` OPEN form(通道词表由写它的那个二进制定),
35
+ * `MemoryErasureAttestation.erased[].binding` 同族。在本层写一张白名单就是**复述一份会随 core 漂移
36
+ * 的判别式**:core 收一格我方不会跟着动,而两边都"绿"。
37
+ * · 与 import 报告那边**刻意相反**(那里是显式白名单):`MemoryImportReport` 是一张**处置清单**——
38
+ * core 新长一个处置座时,静默上 wire 才是危险的,所以那边要编译期差集门。本族两个账是**事实陈述**
39
+ * (这条 entry 是什么、这次删了什么),新长一个事实键的正确行为就是让它到达消费端 —— 少给一个字段
40
+ * 才是这里的危险。两种姿势的判据是「新键静默上 wire 危险吗」,不是「透传省事吗」。
41
+ * `bundle.doc` 那条披露传导的先例在这里同样适用:转发即真话,复述会随版本漂移。
42
+ *
43
+ * ── 幂等:**两条腿的契约不同**,消费端必须分开读(F-5;core 明写)─────────────────────────────
44
+ * · **证据腿**(后端有 `eraseWithEvidence` —— core 自带的 File 后端有):`requestId` 是幂等身份。
45
+ * core 在托管链上落一条 erasure anchor,重发同一个 requestId **读回**那条 anchor 的 pinned id 集,
46
+ * 绝不重新解析;换了选择子还用同一个 id ⇒ 响亮拒(`memory.erasure_selector_mismatch`)。
47
+ * · **降级腿**(`allowUnevidenced: true`,后端没有证据面):core 的原话逐字是 "same requestId = a NEW
48
+ * request (re-resolved — replay convergence is not promised)" —— **重发就是第二次真删**。这条腿上
49
+ * `erasedPreviously` / `evidenceEv` 永不出现,每一行 notFound 都带 `historyUnknown`(「从没存在过」与
50
+ * 「无证据地被删过」不可判别),而 `evidenceCapability: "none"` + `custodyState: "capability-absent"`
51
+ * 是它的自证标记(静默降级是被禁止的形)。
52
+ * 🔴 消费端(SDK / 壳 / 运维脚本)**不许**把这一口当成「重发安全」的口:先读收执的 `evidenceCapability`,
53
+ * `"none"` 时任何重试都必须是人做的决定,不是自动重试策略。
54
+ * ⚠️ **部署形态的诚实话**(照 memory-bundle.ts 的「能力面的诚实形」):今天在本仓,凡是这两口**挂得上**
55
+ * 的部署(= 后端带 `controlPlaneRoot`,即 core 的 File 记忆后端),`eraseWithEvidence` 都在场
56
+ * ⇒ 走的是**证据腿**;两只 SQL 记忆孪生既没有 `controlPlaneRoot` 也没有 `eraseWithEvidence`,
57
+ * 它们的形态是「整口不挂 + 501」,不是「降级腿」。因此降级腿今天是一条**够得着但用不上**的路
58
+ * (调用方传了 `allowUnevidenced: true` 也会被证据腿先接走 —— core 的判序如此)。它成为唯一可达腿的
59
+ * 前提是「某个后端有控制面归属却没有证据面」,那种后端今天不存在;真出现的那天,上面那段幂等契约
60
+ * 就是它的合同,而不是到时候再补一句。
61
+ *
62
+ * `allowUnevidenced` **恒不注入**:server 一个字都不替调用方选 —— 递交的就是调用方发的那个体。降级腿是
63
+ * 一次**人的**授权表态(「我接受没有证据的删除」),中间层替他勾上就是替他签字。
64
+ *
65
+ * ── 验型的分权:server 验**形**,core 验**义**(同 memory-bundle.ts)────────────────────────────
66
+ * 本层只用 zod 校 JSON **结构**(宪法 [2704]:边界必 schema、禁裸 as-cast)。三选一选择子的判决
67
+ * (恰好一个 ids/scope/sessionId、ids 非空去重、allowUnevidenced 是布尔…)**全部**归 core 的
68
+ * `erasureRequestInvalid` —— 在本层抄一遍等于第二真源。于是 `{ids:[…], scope:…}` 这种**形对义错**的请求
69
+ * 会走到引擎并带回 `config.memory_erasure_request`。
70
+ *
71
+ * 计费/lane:两口都是**部署级治理动作**,零模型工作 ⇒ `billable=false`(与 adoption / retention-ops /
72
+ * memory-bundle 同族,申明在 test/billable-route-declaration.test.ts)。
73
+ *
74
+ * ── 改写门(`isCredentialGatedRewrite`):**两口都进** ──────────────────────────────────────────
75
+ * 该门自 #277 起的判据是「持久改写 **或** 授权唯一输入是 principal 头、且**没有属主门兜底**的治理面」:
76
+ * · `POST /v1/memory/erase` 落在**前**一项上,而且是最深的一种:它把条目从本店**删掉**。伪造一个在册
77
+ * operator 头即可对任一租户执行一次不可撤销的删除 —— 无凭证部署上缺的正是「上游会验这个头」这个前提。
78
+ * · `GET …/:entryId/provenance` 落在**后**一项上。🔴 这一条**推翻了 design/316 §3 的原始裁定**
79
+ * (稿子把它豁免在门外,理由是「爆炸半径=一条」);codex 对抗复审 [high] 指出该理由站不住,亲验后采信:
80
+ * ⑴ 稿子援引来类比的两条豁免读(`GET /v1/adoption/:id` / `GET /v1/rules`)在门外的**真正**原因是
81
+ * 它们各有**属主门**兜底,不是「读得少」;本口的授权判据就只有 `explicitOperatorOk`,过了就直接答;
82
+ * ⑵ 无凭证部署上那个头**可伪造**,于是「一条」这个上界实际退化成「知道 id 就能读」——拿一个不可猜的
83
+ * id 当唯一的授权,是本仓在别处明令不接受的形;
84
+ * ⑶ 一条账里有:绑定的 scope+slug、贡献会话 id 与污染理由、仓内 ingest 路径与内容哈希、托管链事件。
85
+ * 先例逐字同源:`POST /v1/memory/export` 当初也「按原措辞本不该进」,同一条论证把它收了进来
86
+ * ——「把它留在门外只为守住一句措辞,是把措辞看得比它要保护的东西更重」。
87
+ * 🔴 判据里**刻意不写**「本口 side-effect-free」:那不是这道门的判据。该门拦的是「授权的唯一输入是
88
+ * 一个可伪造的头」这件事 —— 用「没有副作用」当豁免理由,正是 #277 那次把 `POST /v1/memory/export`
89
+ * (纯读、跨租户全库)漏在门外的那句话。
90
+ * ⚠️ 代价与补偿如实成文:无凭证部署上两口都答 503 `auth.service_token_required`。补偿是**既有旋钮、
91
+ * 零新增** —— 配 `SERVICE_AUTH_TOKEN`(正路),或本地开发显式 `ALLOW_UNAUTHED_WRITES=true`
92
+ * (与 billable / bake / 破坏性会话写 / 其余七扇门共用同一个逃生口)。
93
+ *
94
+ * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
95
+ */
96
+ import type { IncomingMessage, ServerResponse } from "node:http";
97
+ import type { RouteCtx } from "../route-ctx.js";
98
+ export declare const MEMORY_ERASE_PATH = "/v1/memory/erase";
99
+ /** 单条出处读面。带参路径 ⇒ 名册门走 `ROUTE_LABEL_PATTERNS`(server.ts),不是 LITERALS。 */
100
+ export declare const MEMORY_PROVENANCE_RE: RegExp;
101
+ export declare function handleMemoryCompliance(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
102
+ //# sourceMappingURL=memory-compliance.d.ts.map
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+ import { ControlPlaneCorruptError } from "@sema-agent/core";
3
+ import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
4
+ import { sendJson, sendError } from "../send.js";
5
+ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
6
+ export const MEMORY_ERASE_PATH = "/v1/memory/erase";
7
+ export const MEMORY_PROVENANCE_RE = /^\/v1\/memory\/entries\/([^/]+)\/provenance$/;
8
+ const EraseRequestSchema = z.object({
9
+ requestId: z.string().min(1),
10
+ select: z.record(z.string(), z.unknown()),
11
+ allowUnevidenced: z.boolean().optional(),
12
+ });
13
+ function sentCoreCode(res, coreCode, message) {
14
+ switch (coreCode) {
15
+ case "config.memory_erasure_request":
16
+ sendError(res, 400, "config.memory_erasure_request", message);
17
+ return true;
18
+ case "memory.erasure_evidence_unavailable":
19
+ sendError(res, 409, "memory.erasure_evidence_unavailable", message);
20
+ return true;
21
+ case "memory.erasure_selector_mismatch":
22
+ sendError(res, 409, "memory.erasure_selector_mismatch", message);
23
+ return true;
24
+ case "memory.erasure_census_incomplete":
25
+ sendError(res, 409, "memory.erasure_census_incomplete", message);
26
+ return true;
27
+ default:
28
+ return false;
29
+ }
30
+ }
31
+ function codeOf(err) {
32
+ if (!(err instanceof Error))
33
+ return undefined;
34
+ const { code } = err;
35
+ return typeof code === "string" ? code : undefined;
36
+ }
37
+ function sentClassifiedFailure(res, ctx, face, err) {
38
+ const coreCode = codeOf(err);
39
+ if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err)))
40
+ return true;
41
+ if (err instanceof ControlPlaneCorruptError) {
42
+ sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
43
+ return true;
44
+ }
45
+ ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
46
+ return false;
47
+ }
48
+ export async function handleMemoryCompliance(req, res, url, ctx) {
49
+ const miss = { fell: false };
50
+ await handleMemoryComplianceBody(req, res, url, ctx, miss);
51
+ return !miss.fell;
52
+ }
53
+ async function handleMemoryComplianceBody(req, res, url, ctx, miss) {
54
+ const { deps } = ctx;
55
+ const path = url.split("?")[0] ?? url;
56
+ const provMatch = req.method === "GET" ? MEMORY_PROVENANCE_RE.exec(path) : null;
57
+ const isErase = req.method === "POST" && path === MEMORY_ERASE_PATH;
58
+ if (provMatch === null && !isErase) {
59
+ miss.fell = true;
60
+ return;
61
+ }
62
+ const principal = gatedPrincipal(req, deps.config);
63
+ if (deps.config.requirePrincipal && !principal) {
64
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
65
+ return;
66
+ }
67
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
68
+ sendError(res, 403, "auth.operator_only", "memory provenance and erasure are operator-only deployment actions");
69
+ return;
70
+ }
71
+ const faces = deps.memoryCompliance;
72
+ if (faces === undefined) {
73
+ sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
74
+ return;
75
+ }
76
+ if (provMatch !== null) {
77
+ const entryId = ctx.helpers.safeDecode(provMatch[1]);
78
+ if (entryId === null) {
79
+ sendError(res, 400, "request.id_invalid", "invalid memory entry id");
80
+ return;
81
+ }
82
+ try {
83
+ sendJson(res, 200, await faces.provenanceOf(entryId));
84
+ }
85
+ catch (err) {
86
+ if (!sentClassifiedFailure(res, ctx, "provenance", err))
87
+ sendError(res, 500, "internal.error", "memory provenance read failed");
88
+ }
89
+ return;
90
+ }
91
+ const raw = await ctx.helpers.readJson(req);
92
+ const parsed = EraseRequestSchema.safeParse(raw);
93
+ if (!parsed.success) {
94
+ if (parsed.error.issues.some((i) => i.path.length > 0 && i.path[0] === "requestId")) {
95
+ sendError(res, 400, "request.request_id_required", "memory erasure requires a non-empty `requestId` string — it is this request's idempotency identity and the server never mints one for you (a retry must carry the SAME id)");
96
+ return;
97
+ }
98
+ sendError(res, 400, "request.body_shape", "invalid memory erasure request — expected an object with a non-empty `requestId`, a `select` object, and an optional boolean `allowUnevidenced`");
99
+ return;
100
+ }
101
+ if (erasureInputSmugglesPrototypeKey(raw)) {
102
+ sendError(res, 400, "request.body_shape", "invalid memory erasure request — a key that rewrites an object's prototype (`__proto__`) is not a selector key; send exactly one of `select.ids` / `select.scope` / `select.sessionId`");
103
+ return;
104
+ }
105
+ try {
106
+ sendJson(res, 200, await faces.erase(raw));
107
+ }
108
+ catch (err) {
109
+ if (!sentClassifiedFailure(res, ctx, "erase", err))
110
+ sendError(res, 500, "internal.error", "memory erasure failed");
111
+ }
112
+ }
113
+ //# sourceMappingURL=memory-compliance.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * design/378 D2/D3(316 车C)—— 记忆 **consolidation 阀门**的 operator 两口:
3
+ *
4
+ * · `POST /v1/admin/memory/consolidation/run` —— 跑(或**续跑**)一轮,同步回一份收执**摘要投影**;
5
+ * · `GET /v1/admin/memory/consolidation` —— D3 状态投影(阀门、座位、scope 表、上一轮的账)。
6
+ *
7
+ * ── 族属与门序 ────────────────────────────────────────────────────────────────────────────────────
8
+ * 与 `/v1/admin/drain` · `/v1/admin/config/refresh` 逐字同族(operator lane:同一条 `explicitOperatorOk`
9
+ * 门)。门序照 operator 面既有口径:身份(401)→ 授权(403)→ 验型(400)。**授权在验型之前** —— 一个
10
+ * 够不着任何东西的调用方不该从「你的 body 形不对」上读出这台部署有没有这条面。
11
+ *
12
+ * ── 为什么缺席形是 **404 族**而不是 501(D4)───────────────────────────────────────────────────────
13
+ * 本仓的 501 `capability.*` 说的是「**这家店**在本部署上不存在,换部署形态才行」(记忆引擎没接线、
14
+ * 后端没有某个复合面)。本族不是那个形:阀门关着是一句**运维表态**(`MEMORY_CONSOLIDATION_DRIVER=off`,
15
+ * 而且是出厂缺省),不是部署能力的缺失。而阀门开着却结构上跑不了的三种形(引擎没接线 / 后端没有控制面
16
+ * 归属 / provenance=off)在**启动期**就被拒了(`boot/memory-consolidation.ts` 的
17
+ * `assertConsolidationDriverWirable`)—— 那些机器根本起不来,不会有一个"挂着的 501"去代表它们。
18
+ * ⇒ 剩下的唯一缺席成因就是「阀门没开」,而它的诚实形是**这条路径不存在**:挂载条件写在 server.ts 的
19
+ * 域头合取式里,能力位 `memoryConsolidationDriver` 读同一个事实,「说 yes ⟺ 打得通」因此是结构性的。
20
+ * 🔴 刻意与 permissionRules 的 501 分家:那一族的 501 是「规则店没装」,消费端的动作是换部署;本族的
21
+ * 404 的动作是「去把旋钮打开」。同码会让运维照着一条修不好的路走。
22
+ *
23
+ * ── scope **恒不猜**(D2)────────────────────────────────────────────────────────────────────────
24
+ * 一轮 consolidation 读**全库**(~1e5 prompt tokens/轮)。在两个库之间替 operator 挑一个去花这笔钱,
25
+ * 是本设计明确要消灭的形。所以:
26
+ * · 调用方显式给 `scope` ⇒ 用它 —— 哪怕它不在 `MEMORY_CONSOLIDATION_SCOPES` 里。
27
+ * 🔴 那张表**不是授权边界**:本仓今天没有任何授权源能对一个 operator 收窄 scope(`gateMemoryScope`
28
+ * 的 operator 支在查目录之前就 allow;`org-memory-admission` 明写 v1 不中央收窄 operator 声明的
29
+ * scope —— 全文见 `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注)。在本层拿它当
30
+ * 白名单就是**发明第二套授权语义**,与 `gateMemoryScope` 对同一个身份给出相反答案;
31
+ * · 省略 `scope` ⇒ 只有表**恰好一条**时才有缺省(那条就是这台部署的库);零条或两条以上一律 400,
32
+ * 并把候选列进文案 —— 让运维一眼看到「我该点哪一个」。
33
+ *
34
+ * ── 投影纪律:**禁投 LLM 原文**(D3 逐字)────────────────────────────────────────────────────────
35
+ * core 的收执/run 行里有两处**模型作者**的自由文本:`residue[].name`(模型提议的产物名)与
36
+ * `writeFailures[].key`(模型提议的分组键),另有 `planCache.products`(整份 mint 出来的计划)。三处
37
+ * 一律**只上计数**。理由不是洁癖:这两口是 operator 面的**审计读**,而模型作者的文本是**不可信输入**
38
+ * ——把它原样回显给一个运维控制台,等于给一次 prompt injection 开一条到人眼/到日志的直通路。真正要看
39
+ * 原文的场合有正门:mint 归档(`planArchive` 定位符照常上 wire,它是文件名不是正文)。
40
+ * 其余每一格(用量/修复计数/写失败计数/停因/收敛位)都是**核算**数据 —— 那正是这一口存在的理由:
41
+ * 让运维看得见每一轮花了什么。
42
+ *
43
+ * ── 这一口是**分钟级**的持久作业,而且**重试安全** ────────────────────────────────────────────────
44
+ * core 的 run 是 "a persistent job measured in minutes":出厂缺省下一次冷启动折叠是十几个 cycle,
45
+ * cycle 之间有 60s 硬节流。所以 HTTP 客户端可能先超时 —— 那**不是**故障:run 行是 durable 的,同一
46
+ * scope 上有 pending run 时下一次调用 **CONTINUE** 它(同一 plan 缓存、同一 force requestId、**零新
47
+ * mint**,core 的 GD-15 幂等再入)。⇒ 调用方的正确动作是「原样重发」,不是「换个 scope 再试」。
48
+ *
49
+ * 计费/lane:本口**真的烧模型**(而且是本仓单次调用里最贵的一种),所以它进 `isBillableSubmitPath`
50
+ * —— 三道 503 门(drain / model-roster-pending / 无 service-token)必须罩住它。状态读那一口零模型
51
+ * 工作 ⇒ billable=false。两条申明在 test/billable-route-declaration.test.ts。
52
+ *
53
+ * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
54
+ */
55
+ import type { IncomingMessage, ServerResponse } from "node:http";
56
+ import type { RouteCtx } from "../route-ctx.js";
57
+ export declare const MEMORY_CONSOLIDATION_STATUS_PATH = "/v1/admin/memory/consolidation";
58
+ export declare const MEMORY_CONSOLIDATION_RUN_PATH = "/v1/admin/memory/consolidation/run";
59
+ export declare function handleMemoryConsolidation(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
60
+ //# sourceMappingURL=memory-consolidation.d.ts.map