/** * [ref] 件③ —— `GET /v1/memory/entries/:entryId/provenance` · `POST /v1/memory/erase` 两口的 * **引擎接线**(core 5.57.0 `MemoryEngine.provenanceOf` / `eraseMemoryEntries`,[ref] v2-a/v2-b)。 * * ## 为什么是一个模块而不是 main.ts 里直接 new * * 同 `memory-bundle-engine.ts` 的那句话:本仓**没有**一个长命的 `MemoryEngine` 实例 —— `boot/stores.ts` * 装配出来的 `memoryEngine` 是 `{ backend, root }` 二元组,真引擎由 core 的 Runner **每任务现构** * (按任务的 scope 平面决定 `memoryDir`)。HTTP 面若要一个引擎,必须自己构;构的判断写在这里,main.ts * 只调用与转接。 * * ## 控制面归属:为什么 `controlPlaneRoot` 缺席就整口不挂(F-9 裁) * * 这两条路径与 bundle 那两条**不同**:它们**真的读引擎控制面**。亲读 core 5.57.0 `engine.js`(不信 JSDoc, * 读实现):`provenanceOf` 读 `challengedEntryIds(this.controlDir)` / `lineageAccountOfEntry(this.controlDir, …)`; * `eraseMemoryEntries` 的降级腿读 `lineageContributionsOfSession(this.controlDir, …)`,证据腿则整段跑在 * backend 自己的 `controlPlaneRoot` 上(托管链 `transfers.jsonl`、erasure anchor)。 * * 而 `this.controlDir` 的来源是 core 构造器的这一支(engine.js 逐字):`backend.controlPlaneRoot` * 在场就用它,**否则**回落到 `deriveControlPlaneDir(resolveMemoryEngineRoot(), memoryDir)` —— 一个从**本进程 * 的本地盘**派生出来的目录。本仓是 stateless replicas(CLAUDE.md 首段):回落形下,erase 的 write-ahead * 托管行(崩溃窗内条目字节的唯一一份)会落到某个随机 pod 的本地盘上,pod 重建即失,而且操作面引擎与 * Runner 每任务现构的引擎会看到**两个不同的控制面**。 * * ⇒ 裁:`controlPlaneRoot` 缺席 ⇒ 本工厂返回 **undefined**,两口整个不挂载,HTTP 面诚实 501。今天满足这一 * 条的只有 core 自带的 `FileMemoryEngineBackend`(它把控制面写死在 config-root 一侧);本仓两只 SQL 记忆 * 孪生(`plugins/memory-engine-{pg,tidb}.ts`)没有这个面 ⇒ 在那些部署上两口不挂。SQL 控制面是 core 的候升件 * ([ref] §0b-4 咨询单),**绝不**在本层为它们现造一个「够用的」控制面 —— 那是把一个 core 拒绝生产的 * 半吊子治理面自己造出来,与 bundle 那条「a governance-less export is the laundering shape」同族。 * * 🔴 这条判断是承重的,所以它有**机器钉**而不是只有这段注释:`test/memory-operator-faces.test.ts` 用一只 * 假 backend 断言 ①缺 `controlPlaneRoot` ⇒ undefined;②给一个**不存在**的 `memoryDir` 两口照样答得出来 * (⇒ 它没被读);③backend 缺审计/托管面时 core 自己答 `capability-absent` 三值,本层零修饰。 * * ## 能力缺席在**构造期**判定(与 bundle 三件套逐字同形) * * 判据是一次**属性检查**、boot 期就判得出、而且是**永久**缺席(换后端才会变)——不是瞬时状态。照旧挂着 * 就成了「能力位说 yes、每一次调用都确定性失败」,而本仓「says yes ⟺ route works」是结构性承诺。 * (这条纪律是 [ref] 提货批被 codex 交叉复审驳倒后立的,见 memory-bundle-engine.ts 头注。) * * ⚠️ **刻意不把审计/托管/证据三个可选面编进构造判定**:`committedSnapshotOf` / `custodyOf` 缺席时 core * 明写要诚实答 `binding:"unknown"` / `contentState:"capability-absent"` / `custody:"capability-absent"` * ([ref] absence-reports-not-silent-green,engine.d.ts 逐字),那是一个**答得出话**的部署;把它们编进 * 构造判定会让这种部署整口消失,把 core 的诚实答案换成一句 501 谎话。`eraseWithEvidence` 同理:缺它时 * core 自己响亮拒(`memory.erasure_evidence_unavailable`),那是**运行期**的诚实拒,与「换部署形态才行」 * 不是一回事(附录 A 两条分列)。 */ import { type EntryProvenanceAccount, type MemoryBackend, type MemoryEntryOrigin, type MemoryErasureAttestation, type OriginClearanceRow, type SessionCaptureRecordStore, type SessionMemoryStatus } from "@sema-agent/core"; /** * 控制面归属判据 —— 三个记忆操作面(合规两口 / 外源标记三口 / consolidation 阀门的 D1 拒启门)共用的 * **同一只**谓词:backend 的面上带**非空** `controlPlaneRoot` 才算归属在案。空串与缺席同判「不归属」: * core 对空串会 truthy-回退到本副本盘上派生控制面(engine 构造里的 `backendControl ? … : derive(…)`), * 对无状态多副本部署这正是 D1 要拒的那台机器(折叠账随 pod 一起死)。抽成单点导出是为了让三处 * 「逐字同源」成为结构事实而不是手抄承诺(merge-rescan 746 C-1:main.ts 手抄版漏了空串臂)。 */ export declare function backendOwnsControlPlane(backend: unknown): boolean; /** 两口 HTTP 面消费的窄能力面(`ServiceDeps.memoryCompliance` 的实参来源)。 */ export interface MemoryComplianceFaces { /** 单条出处账。core 的失败姿态原样穿过:控制面损坏 ⇒ `ControlPlaneCorruptError`(fail-closed, * 绝不在一个完整性未知的账上拼一个答案);能力缺席 ⇒ 三值 `capability-absent`,**不是**抛。 */ provenanceOf: (entryId: string) => Promise; /** * 抹除。入参 `unknown`:调用方递交的是未校验的对象,三选一选择子的整体判决归 core * (`erasureRequestInvalid` 是它的单一属主面)—— 在本层抄一遍就是第二真源。 * * `opts.allowedScopes` = **授权信封座**(见 {@link erasureEnvelopeRefusal} 的头注)。 */ erase: (input: unknown, opts?: { allowedScopes?: readonly string[]; }) => Promise; } /** * **选择子替身**的拦截谓词(codex 交叉复审 [critical],亲验复现后修 —— 复现器与结论逐条在 * `test/memory-compliance-http.test.ts` 的同名 describe 与 `test/memory-operator-faces.test.ts` 里)。 * * ## 病(在 core 5.57.0 上实测,不是纸面推演) * * `{"select":{"scope":"user:harmless","__proto__":{"ids":["victim"]}}}` 这个体: * · `JSON.parse` 把 `__proto__` 建成 select 的**自有数据属性**(JS 对象字面量里写不出这个形 —— * 那里的 `__proto__:` 是设原型的语法糖,所以这条路只能由**原始 JSON 文本**走进来); * · core 的 `captureErasureInput`(file-backend.js)用**赋值**把每个自有键抄进 `{}`,抄到 * `__proto__` 那一下改的是克隆体的**原型**,于是这个键从 `Object.keys` 里消失; * · core 的 `erasureRequestInvalid` 数的是 `Object.keys` ⇒ 只看见一个 `scope` ⇒ **判合法**; * · 而两条执行腿的分派都写作 `"ids" in select`(engine.js 降级腿、file-backend.js 证据腿), * `in` **查原型链** ⇒ 继承来的 `ids` 赢,删的是不当调用方点名的那批 id; * · 收执里的 `select` / `selectHash` 记的却是那个自有的 `scope`。 * ⇒ **删掉的东西与凭据上写的不是同一批**,而这一口的全部意义就是那份可归档的凭据;删除不可回滚。 * * ## 修在哪一层(为什么不是「替上游打补丁」) * * core 的复制与分派确实该收紧(own-property 检查 / null 原型克隆),那是**上游工单**。本层这条谓词不 * 改上游语义、也不替它清洗数据:它做的是**我方边界该做的事** —— 一个会重写对象原型的 JSON 键**不是** * 一个选择子键,我方不把这种体递交下去,而且**响亮拒**(不是静默抹掉那个键:安全轴上的静默修正正是 * 本仓禁止的形,而且悄悄改写调用方的请求会让它以为自己删的是别的东西)。 * * 两个执法点共用**这一只**谓词:路由拿它出 400(wire 可见、有钉),seam 拿它 fail-closed 兜底(seam 是 * 可导出模块,不能靠「今天只有一个调用方」这句话来安全;走到那一臂 = 路由的门被跳过了 = 装配 bug, * 500 fail-loud 是对的)。刻意不做「只在 seam 拦」:那样 wire 上就没有一句能测的诚实拒。 * * 走法是**迭代**而非递归:体的深度只受体积上限约束,一个深嵌套的 JSON 能把递归版打爆栈(把一个 * 完整性缺陷换成一个可远程触发的崩溃不算修好)。 */ export declare function erasureInputSmugglesPrototypeKey(input: unknown): boolean; /** * 在**已装配的记忆后端**上建合规两面。命名照 CLAUDE.md 工厂律:返回的是带行为的活对象 ⇒ `create*`。 * * `memoryDir` = 引擎的**挂载平面**(`memoryMountRootOf(backend, root)` 单铸点:后端钉了 `directoryRoot` * 就是它 —— file 腿 = `<配置根>/memory`;两只 SQL 孪生不钉 ⇒ 回到配置根,读数逐字节不变)。 * * 🔴 **不是 `memoryEngine.root`**(2026 提货批订正,亲读 core 推翻旧注):旧注写着「这两条路径不读它」, * 而实装里 `erase` 的派生索引清扫(`engine.js` 的 `sweepErasedIndexLines`)与 `clearEntryOrigin` 的 * 盘上分歧判据(`completeOriginClearance`)都按 `scopeDirFor(this.memoryDir, …)` **真的**去读挂载树 —— * 只是文件头 ② 号钉用的收执里 `erased: []`(清扫提前返回),把这件事测成了恒绿。交配置根的后果是 * **静默**:扫一棵不存在的树 ⇒ ENOENT ⇒ 跳过 ⇒ 零残留可报,而真正的 `MEMORY.md` 里还留着被抹条目的 * 名字,收执却显示干净。机器钉:`test/memory-operator-faces.test.ts` 的「[ref] 提货」两格。 */ export declare function createMemoryComplianceFaces(store: { backend: MemoryBackend; root: string; }, /** * 引擎的 **advisory incident 座**(core `MemoryEngineOptions.onIncident`)。接它不是锦上添花: * 抹除腿上有一条**只走这个座**的码 —— `memory.erasure_index_residue`(engine.js)。它说的是 * 「这次抹除的**索引残留披露**没能写进收执」:于是 operator 归档的那份凭据里少了一句「MEMORY.md * 里可能还留着被删条目的名字」,而收执**看上去是干净的**。座不接 ⇒ 这句话在本部署上无声消失,正是 * 安全轴上禁止的那种静默(合规腿尤甚)。同座还带 `memory.announce_failed`(公告队列本身写不进去)/ * `memory.challenge_ledger_corrupt` / `memory.lineage_settle_failed`。 * ⚠️ core 的 d.ts 只列了两个码走这个座 —— **亲读实装**(engine.js 的四处 `sink(e)`)不止两个, * `memory.erasure_index_residue` 正是它漏写的那条(宪法:不信 JSDoc,锚实现行)。机器钉在 * `test/memory-operator-faces.test.ts`,用一份 residuals 被冻住的收执把那条腿确定性打出来。 * 缺席合法(advisory-silent,与 bundle 三件套同形):宿主不接就只是没人听,引擎不会因此失败。 */ opts?: { onIncident?: (err: Error & { code?: string; }) => void; captureRecordStore?: SessionCaptureRecordStore; }): MemoryComplianceFaces | undefined; /** 件② 三口 HTTP 面消费的窄能力面(`ServiceDeps.memoryOriginFace` 的实参来源)。 */ export interface MemoryOriginFaces { /** * §13-4① 视图面:点名 scope 里**每一条**带外源标记的条目 + 它的出处账。 * * 🔴 `scopes` 由**调用方**给,本层不发明默认值:v1 本仓没有任何 scope 枚举读面(core 5.59.0 的 * `listMemoryScopes` 已到 File 腿、SQL 腿是 core 候升件 [ref]④,本仓尚未接线),而「空 scope 名单」 * 与「查全店」在这条路上是两件事 —— 猜一个默认名单会让审计的**不完备变成静默的**(空数组与 * 「这些 scope 干净」同形)。缺参时路由那侧 400 并指路,理由与文案同源于此。 */ listExternal: (scopes: readonly string[]) => Promise>; /** §13-4② 读半场:清标审计账(**同步** —— core 的 `listOriginClearances` 就是一次严格 sidecar 读; * 账本损坏 ⇒ `ControlPlaneCorruptError`,fail-closed,绝不回半张表)。 */ listClearances: () => OriginClearanceRow[]; /** §13-4② 写半场:审计化的 UN-MARK 阀门。拒因族是 core 的**闭集码**,本层零翻译原样上抛。 */ clear: (entryId: string, input: { requestId: string; reason: string; }) => Promise<{ entryId: string; clearanceId: string; origin: MemoryEntryOrigin; landedSlug: string; }>; } /** * [ref] 件② —— 外源标记人面的**引擎接线**(core 5.59.0 `MemoryEngine.listExternalOriginEntries` / * `listOriginClearances` / `clearEntryOrigin`,[ref] §13-4①②)。 * * ## 控制面归属:判据与合规两口**逐字同一条**(§1 F-9 裁),但理由更硬 * * 亲读 core 5.59.0(不信 JSDoc,读 `engine.js` 实装): * · `listOriginClearances` = `readOriginClearances(this.controlDir)`; * · `clearEntryOrigin` 整条弧都压在 `this.controlDir` 上 —— 写前开行(`openOriginClearance`)、 * 墓碑落定登记(`markOriginClearanceTombstoned`)、终态事件(`settleOriginClearance`); * · `listExternalOriginEntries` 每一行都要 `provenanceOf`,而它读 `challengedEntryIds` / * `lineageAccountOfEntry`,同样是控制面。 * 三个动词没有一个能离开控制面。而 `clearEntryOrigin` 的写前行里躺着 `entryText` —— * **被清标条目的完整正文**;core 的原话逐字是「A crash between the tombstone batch and the re-record * batch leaves this as the only copy」(origin-clearance.d.ts)。也就是说:墓碑已落、重录未成的那个 * 崩溃窗内,这一行是那条记忆在世界上**唯一的一份**。 * * ⇒ 在 `controlPlaneRoot` 缺席的部署上(core 会把控制面落到 `resolveMemoryEngineRoot()` 派生的**副本 * 本地盘**,engine.js),这唯一的一份会落在某个随机 pod 的本地盘上,pod 重建即**永久丢失**, * 而且下一次 resume 调用大概率落到另一台副本、读不到那行 ⇒ 一条被清标的记忆连同它的托管字节一起消失。 * 本仓是 stateless replicas(CLAUDE.md 首段)。所以判在**构造期**:工厂返 undefined、三口整个不挂、 * HTTP 面诚实 501,与 `createMemoryComplianceFaces` 同形同码不同文案(要查的旋钮不是同一个)。 * SQL 控制面是 core 的候升件([ref] §0b-4 / [ref]④),**绝不**在本层为它现造一个。 * * ## 刻意不做的两件事 * * ⚠️ **不把 `listScopes` 编进构造判定**:core 5.59.0 长出了 `listMemoryScopes`([ref]③,File 腿在场、 * SQL 腿候升),但那是**发现链**的面,不是本族三口的执行前提 —— 把它编进来会让一个「scope 名由运维 * 自己拿着」的部署整口消失。发现链的不完备由 `GET …/external` 缺参时的 400 文案如实说出来。 * ⚠️ **不在 seam 里翻译拒因码**:core 的 origin-clear 族是**闭集**,翻译点只有一个(路由的 switch), * 在这里再翻一遍就是第二真源。seam 原样抛,路由逐码字面量转发。 */ export declare function createMemoryOriginFaces(store: { backend: MemoryBackend; root: string; }, /** * 引擎的 advisory incident 座(core `MemoryEngineOptions.onIncident`)。 * 🔴 与合规面的同名座**理由不同,如实写**:亲读 core 5.59.0 的这三条动词路径(engine.js) * 一个 `sink(...)` 都没有 —— 今天没有任何码**只**走这个座上来。接它是因为代价为零而失效形是静默的: * core 哪天在这条路上加一条只走 sink 的披露(清标的族里最像的候选是账本尺寸/重建那两条),不接的 * 部署会让它无声消失。缺席合法(advisory-silent,与 bundle / 合规三件套同形)。 */ opts?: { onIncident?: (err: Error & { code?: string; }) => void; captureRecordStore?: SessionCaptureRecordStore; }): MemoryOriginFaces | undefined; /** [ref] seam① 的窄能力面(`ServiceDeps.sessionMemoryStatus` 的实参来源)。 */ export interface SessionMemoryStatusFace { status: (sessionId: string) => Promise; } /** * [ref] seam①([ref] §S-7,core 7.0.2 [ref] 件1)—— `GET /v1/sessions/:id/memory-status` 的引擎面。 * 命名照 CLAUDE.md 工厂律:返回带行为的活对象 ⇒ `create*`。 * * 挂载判据与合规/清标两族**同一条 F-9 裁**(`backendOwnsControlPlane`,理由见文件头): * `committedCount` / `foldedCount` / `lastCaptureAt` 读的是引擎控制面的 lineage 台账 —— * 控制面归属缺席的部署上(两只 SQL 记忆孪生),core 会把控制面落到**副本本地盘**,一个「可读的空目录」 * 让 core 面答出 `committedCount: 0`(readLineageRecord 对空目录**不抛**,亲读 engine.js)—— * 那是**说谎的 0**,比缺席更坏(S-7 的诚实键律:可读零必须是真零)。⇒ 归属缺席 ⇒ 工厂返 undefined, * HTTP 面诚实 501,与 erase/origin 同码同分支。 * * `captureRecordStore` = [ref]② 的 SQL 载体(main.ts 单实例,与 Runner/其余引擎同源 —— 全站同批注入律, * 机器钉 capture-optout-lane ②):`captureOptedOut` / `optOutSource` 两键经它读,多副本部署上答的才是 * 跨副本的真记录。S-215 起 local 车道在引擎接线时也供(core 文件三腿,同一只控制面,单机字节不变);缺席 ⇒ 记忆面暗。 * * 面**不抛**(core 契约:sessionMemoryStatus never throws —— 每个事实源各自 catch 折缺席键); * 不接 onIncident:这条只读路径上今天零 sink 站点,而且缺席键本身就是它的披露形。 */ export declare function createSessionMemoryStatusFace(store: { backend: MemoryBackend; root: string; }, opts?: { captureRecordStore?: SessionCaptureRecordStore; }): SessionMemoryStatusFace | undefined; //# sourceMappingURL=memory-operator-faces.d.ts.map