Skip to content

[Feature] 写入准入(non-write 判定):把「这条该不该进库」前移到 LLM 之前 #254

Closed

Description

@modusensus

背景

2608.22215(Dual-Layer)的映射条目指出我们缺一块机制:写入准入。

现状核对:

现有 位置 性质
saveWithDedupe src/service.js 只有 write-new / write-update 两分支——默认「都写」
quality-filter src/quality-filter.js 写入后评分再归档,不是写入前拒绝
密钥/PII 扫描 #164 A2(未实现) 属于同一设计词的一类判据

从成本视角看这件事更清楚:#239 的第 1 项在蒸馏前拦窗口,省的是那次调用的输入;而写入准入是在更上游决定「这条根本不该进库」,省掉的是后续所有环节(写入、抽取、巩固、注入槽位、检索噪声)。量级可能大于窗口级预判。

方案

三级判定,全部留审计,默认关:

  1. 确定性拒绝(零 LLM):密钥/PII 模式命中(与 聊几个我觉得值得做的方向 #164 A2 合流——A2 是本级的一类判据而不是另起一套)、空白或纯噪声、命中已有条目的去重键;
  2. 规则放行(零 LLM):显式高价值信号(importance 显式给出、用户偏好陈述、决策动词 + 实体);
  3. 不确定 → 交 LLM:维持现状路径。

每次判定写一条准入决策回执(拒绝/放行/升级 + 原因),沿用 llm_audit_logs 的 skip 口径,使「这条为什么没进库」可查。

验收

  • 默认关:零行为变化(默认路径逐字节一致)——writeAdmission.enabled / enforce 与 sensitiveScanEnabled 均默认 false,settings.js 白名单成对注册;只计量阶段的行为逐字节保留(config.js:806 注释记着 feat(admission): 第 1 级确定性拒绝——密钥/PII 与空白噪声硬拒(#254) #332 合并时 sensitiveScan 就是 null);
  • 开启后被拒绝的写入有审计与明确原因,且可解释(不是静默丢弃)——拒绝走 write-admission.js:349 status: "skipped",审计沿用 llm_audit_logs 口径,metadata.deny.reason 是稳定键(blank / noise / sensitive,语义各自分开报);
  • 密钥/隐私样例不漏放(配回归样本集,聊几个我觉得值得做的方向 #164 A2 复用同一套)——test/write-admission-samples.js 26 条样本集 + sensitive-scan.test.js 16 用例(第一条用例就是样本集整体跑真判据:正样本不漏、负样本不误杀);feat(admission): 写入边界的密钥/PII 判据(#164 A2 → #254 第 1 级) #354 顺手补了 AWS secret access key 的键名规则缝;
  • 与 聊几个我觉得值得做的方向 #164 A2 的关系写清楚:A2 是第 1 级的判据来源,不是并列的第二个机制——A2 即第 1 级的 sensitiveScan 判据(src/index.js:330 接线注释写明口径),独立顶层键 sensitiveScanEnabled(默认关、开了也仅告警;真拦还需 writeAdmission.enabled + enforce);
  • 新配置键按 config.js schema + settings.js 白名单成对出现,旗标计数锁同步——writeAdmission 两键与 sensitiveScanEnabled 成对入白名单,计数锁同步。

落地记录(对照 main,2026-11-10)

批次:7fd9be7(只计量不拦截)→ 52dee2f(第 1 级确定性硬拒)→ 029dd5a(内容哈希计量 + 归档行纳入去重候选)→ 8dc2a59(A2 密钥/PII 判据,PR #354)→ 0e45a61(复审:启动期审计清理读装配后的 cfg)。测试:write-admission.test.js 35 用例 + sensitive-scan.test.js 16 用例。

还剩什么(开单理由的存续部分)

  1. G1/G2 阈值(会话写入预算 / 同话题重写间隔):故意只计量不拍数,等 llm_audit_logs 攒出真实分布(数百活跃会话量级)后在本帖贴分布,再提第二阶段(阈值 + memory_save({confirm:true}));
  2. A2 覆盖面:判据目前只挂会话写入路径(service.js:1566 的准入汇流点,判据要求 sessionKey 在场)——autoSummarize 与 dream 输出两个出口未接入(feat(admission): 写入边界的密钥/PII 判据(#164 A2 → #254 第 1 级) #354 评论区提的「第二批」,未做),共享包导入、bootstrap 这类无 sessionKey 的写入同样不在覆盖内。

参考

Activity

  1. heptaspirit commented on Sep 21, 2026

    @heptaspirit
    Collaborator

    更新:下面另发了一条第二版整合稿(行号换成符号锚,末尾补了次数缺口一节)。这条保留原样。

    正文现状表三条我按 upstream/main @ 360f7c8 逐条核对过,现在仍成立。附一下代码位置;另外有两个点会改变实现口径,想先请你定。 现状核对 - saveWithDedupe 只有两支:命中合并(src/service.js:1183),否则新建(:1245)——没有拒绝分支,表里「默认都写」准确。 - memoryQualityFilter 在 src/service.js:1143 评分,处置却落在写之后(合并路径 :1239、新建路径 :1264,applyQualityDisposition 定义在 :1287),默认开(src/config.js:478)。它确实不是写入前拒绝。 - 密钥/PII:upstream/main 的 src/ 里 grep secret|PII|redact|api_key 零命中。 - 审计与旗标面可直接复用:llm_audit_logs(src/store.js:181)已有 status: success|error|skipped 与自由 metadata,写入口 saveLlmAudit(:1746);旗标三处是 src/config.js 的 schema、src/settings.js:42,91(嵌套键按点号平铺)、lib/client.js:1830 的 FEATURE_GROUPS 加双语文案,计数锁在 test/api.test.js:667。 两个口径 1. 第 1 级「命中已有条目的去重键」判 non-write,和现有合并语义冲突。 现在同 title 命中就走 append 合并(src/service.js:1192;_overwrite: true 时直替,:1189),按你的三分类它属于 write-update。若去重键命中一律判 non-write,等于把合并一起取消;若判 write-update 放行,第 1 级就只剩密钥/PII 与空白噪声两类判据。我倾向后者,但「调用方声明新建」与「默认合并」要不要拆成两个入口,得你定。 2. 第 1 级把同步去重从「合并」升级成「拒绝」,会放大一个次序耦合。 我最近读到 2606.24535(Governed Shared Memory for Multi-Agent LLM Systems),里面记了一个同构事故:同步的近重复门抢在异步矛盾检测之前生效,本应被裁决的矛盾写入被门直接拒掉,裁决对象根本进不了库——它把这一类列为四个失效模式之一。mneme 这条路上正好是 saveWithDedupe(同步)与 autoDream 冲突检测(异步)前后相接。所以「去重键命中」这条判据走「拒绝」还是走「合并」,风险不是一个量级;文献那边给的修法是去重门只标记、不拒绝。 如果第 2 点你也认为是同一类风险,第 1 级的「去重键命中」可以降级为标记 + 记审计(不拦),硬拒绝只留密钥/PII 与空白噪声。这样第 2 级的规则放行也不必和合并抢同一个判定面。 最后问一下要不要我搭把手,这个功能的面我这边已经核对过,拍板后就可以开发了。 @modusensus

  2. heptaspirit commented on Sep 22, 2026

    @heptaspirit
    Collaborator

    @modusensus

    这是第二版,把 09-21 那条整合重排了一遍,末尾补两节那次没覆盖的东西。

    现状核对

    • saveWithDedupe 只有两支:命中合并(src/service.js › saveWithDedupe),否则新建,没有拒绝分支;表里「默认都写」准确。
    • memoryQualityFilter 是评分之后在写入之后处置(默认开),不是写入前拒绝。
    • 密钥 / PII 在 src/ 全树检索零命中,第 1 级目前没有现成判据可用。
    • 审计面可复用 llm_audit_logs(status: success | error | skipped + metadata)与 saveLlmAudit;旗标三处 = src/config.js schema、src/settings.js 白名单、lib/client.js 的 FEATURE_GROUPS,计数锁在 test/api.test.js。

    两条口径

    1. 第 1 级剩两类判据。 「命中已有条目的去重键」判 non-write 与现有合并语义冲突:同 title 命中走 append 合并,判 non-write 会把合并路径变成拒绝路径。倾向判 write-update 放行,第 1 级只剩密钥 / PII 与空白噪声。

    2. 去重门也走二次确认,不是硬拒。 把同步去重从「合并」升级成「拒绝」会放大次序耦合。文献 2606.24535(Governed Shared Memory for Multi-Agent LLM Systems)把这类事故列为失效模式之一:同步的近重复门抢在异步矛盾检测之前,把本该被裁决的矛盾写入直接拒掉。这里的次序是 saveWithDedupe(同步)在前、autoDream 冲突检测(异步)在后,正好是那个形状。

      我原先写的是「只标记、不拒绝」,现在觉得二次确认更好,但它有一个前提:提示文案必须把「这是一条需要裁决的矛盾」写成二次触发的合法理由。否则它照样退化成那个失效模式:AI 收到「已有相似条目」之后默认放弃,矛盾被静默丢掉。二次确认在这里的作用是让人停一下,不是让人写不进去。

    补一节:收窄之后漏出来的次数缺口

    按上面收窄,第 1 级只拦密钥 / PII / 空白噪声。那「同一件事反复记」就没人管了:去重键是 type + title + scope 三元组,换标题绕过 title 维,换 type 绕过 type 维;归档行不参与比对,所以一条被质量闸归档之后,同样的内容还能再写一遍。

    建议再加两道闸门,同样走二次确认:

    • G1 会话写入预算:一个会话内新建条数上限 N。超限后第一次调用不写库,返回原因与可自查的提示;AI 判断确实该记,需显式二次触发才放行。
    • G2 同话题冷却:同一话题(issue 锚、文件路径、已存在的 document 行)在 X 轮内已写过,就转成追加到已有行或已有 document,不新建行。

    不做硬拒,因为硬拒在 AI 不知道闸门存在时会直接丢掉信息,而闸门一旦做内容判定就有误杀面。二次确认只加一次成本,且成本随叠加次数递增,越叠越贵。

    两条都要留两个穿透口:用户显式要求「记住这个」,以及 pinned(constraint / preference)。后者与 #275 拍板 5 的 pinned 豁免同口径。

    二次确认一次服务三条线

    这个形态不只是 #254 的闸门,它在另外两条线上各占一个位置:

    三处要用同一个形态,就值得把接口一次定清楚,而不是三个 issue 各做一套。这也是我把这件事写在 #254 而不是拆成三条的原因。

    需要拍板

    1. N 与 X 的取值:建议先记录不拦,由遥测定,而不是先定一个数。
    2. 纯合并(未新增行)算不算进会话预算。
    3. 二次确认怎么标记「这是第二次」:显式参数(memory_save({ confirm: true }))、返回 token 带回,还是同话题第二次隐式放行。
    4. 提示文案里要不要显式写明「需要裁决的矛盾」也是合法理由(第 2 条口径的前提)。
    5. 要不要一道全局写入速率上限,而不只是会话级。
  3. modusensus commented on Sep 23, 2026

    @modusensus
    MemberAuthor

    @heptaspirit 第二版收到,两条口径都同意,五个拍板项逐条定:

    1. N 与 X 的取值:同意「先记录不拦,由遥测定」。 落地顺序定为两阶段:第一阶段闸门只记审计行(复用 llm_audit_logs 的 status:'skipped' + metadata 标 gate:g1/g2),不拦截、不改变任何行为——这一阶段对用户零风险,也正好产出第 5 点要的水位数据;等遥测有了真实分布(会话内新建条数的 P95、同话题重写间隔),第二阶段才把拦截打开(届时按仓库惯例 opt-in 默认关 + settings.js 白名单 + lightMode 考虑)。不要在没数据时拍一个数。

    2. 纯合并不算预算。 会话预算管的是「库里多了几行」,合并是去重机制在正常工作——把合并算进去等于惩罚系统做对了的事,还会让 AI 学会规避合并(换标题绕开,正好制造你要防的那类行)。只计新建行。

    3. 二次确认用显式参数:memory_save({ confirm: true })。 三个候选里只有它把「这是第二次、AI 确认过理由」变成审计行里可读、可回放的事实;返回 token 带回依赖 agent 恰当管理上下文(脆),同话题第二次隐式放行则把「停一下」退化成「多调一次」——隐式方案下模型重试的性价比高于思考,闸门就名存实亡。显式参数还让拦截与放行在审计里成对出现(第一次 skipped/gate → 第二次 confirm:true 放行),回放时因果完整。

    4. 要,而且是硬要求。 提示文案不写明「需要裁决的矛盾也是合法理由」,二次确认就一定退化成 2606.24535 那个失效模式——这不仅是「要不要写」,是第 2 条口径成立的前提条件,验收时把这条文案当规格审(同款红线还有:文案描述的行为必须与工具实况一致)。

    5. 全局速率上限:不做。 会话预算 + 话题冷却已覆盖已观察到的失效形态(单会话内跑飞),全局上限没有数据支撑它拦的是真问题而不是正常重度使用(冷启动、批量导入这类合法场景都在会话外),多一道闸多一个误杀面。等遥测出现跨会话洪水的证据再议。

    两个穿透口(用户显式要求记住、pinned 的 constraint/preference)确认,与 #275 拍板 5 的豁免同口径;G1/G2 的实现 PR 按三处共用一个接口的思路走(#249 注入时机、#275 水位遥测、本单闸门),接口形状在实现 PR 里一次定清,不三个 issue 各做一套。

  4. heptaspirit commented on Sep 23, 2026

    @heptaspirit
    Collaborator

    @modusensus

    按你拍的口径,补两个实现侧的信号选择。

    计量信号用内容哈希,不用相似度。 「先记录不拦」要记录什么,决定了遥测能不能用。相似度阈值在这里天然不可分:同一件事换个说法就掉下去,不同的事共享话题词又顶上来,阈值往两边挪都是在误判之间来回。把 title 与 content 先归一化(大小写、空白、标点)再取哈希,命中就是精确重复,零成本,只落一个标记列,正落在「记录不拦」里。归一化口径要一次定死,后面改要带存量重算策略。

    去重候选集把归档纳入。 你在那节点到了,我确认一遍:归档行不参与比对,所以一条被质量闸归档之后,同样内容还能再写一遍,去重键是新的,没有任何东西拦它。按结构化锚召回可以先收窄成本,不必每次全扫。两个穿透口与你说的 pinned 豁免同口径。

  5. modusensus commented on Sep 24, 2026

    @modusensus
    MemberAuthor

    @heptaspirit 两条都收,按这个做:

    1. 计量信号用内容哈希,同意。 相似度阈值在「只记录」阶段的不可分你论证得够清楚——记录阶段的判据要的是零成本、可复算、无争议,归一化哈希三条全占。两个前置照你说的办:归一化口径(大小写、空白、标点)一次定死并在代码注释里写明动机;后续若要改口径,带上存量重算策略再动,标记列只加不改语义。

    2. 归档行纳入去重候选集,同意。 这正是 #275 楼里说的分界的另一半——归档出口止体积、不止重复,剩下的穿透口就靠去重候选集兜住。结构化锚先收窄比对成本、不做全扫,方向对;两个穿透口与 pinned 豁免同口径,一并在实现里写清。

    两条都落在第一阶段「只记录」里,不改变拦截行为,不需要新立项。#305 我这边接下来逐个 update-branch 走审查合并。

  6. added 2 commits that reference this issue on Sep 24, 2026
  7. heptaspirit commented on Sep 24, 2026

    @heptaspirit
    Collaborator

    @modusensus

    三条跟这一批相关的补充。

    一、G2 现在测的那个现象,代价在注入侧。 同话题重复新建出来的行不是重复文本,是同一件事的不同侧面,所以按现在的去重键它们不会、也不该被拦(跟你定的「纯合并不算预算」同理)。它们真正的代价在注入面:单轮只有 maxInjectedItems 条(默认 5),一族长到几十条之后,能给出「这件事现在按什么口径走」的概率就低了。这个后果面该由整理收,不在这批里,我在 #231 补了机制。

    二、候选集的锚还能多一类:实体关联。 你给去重候选集定的口径是「按结构化锚召回先收窄成本,不必每次全扫」,当时用的是话题引用与文件路径。抽取开着时还有一类现成的锚:entities / entity_attrs / entity_relations 三表都带 memory_id,而 store.getLinkedMemoryIds(entityIds) 已经把「实体集合 → 记忆集合」这一步做好了(当前有效属性行直挂的记 attr 档、关系行挂的记 relation 档,同一记忆取更强的 attr)。同一个锚既能喂去重候选集,也能喂 #231 的整理候选,不必各造一套。要注意两个前提:抽取默认关(entityExtractionEnabled),以及 entityRecall 现在把归档行排除,而升格吸收不会把属性行迁走(document.js 不碰实体表,对比 decisions.js 的 migrateAttrsToMemory),所以「锚到归档行」这条路今天是不通的。

    三、CHANGELOG.md 里有一句普适表述站不住。 「写入准入第一阶段」一节写「会话内新建条数 按会话直接计数即得」,但计量点落在 llm_audit_logs 上,而计数函数 countLlmAudits({ source, sessionKey }) 只过滤来源与会话键两个维度:豁免行(metadata.exempt='pinned')与 G2 的同话题行共用同一会话键,所以这句话只在「没有豁免行、也没有同话题重复」的场景成立。代码注释给的补救是调用方自己加一个排除豁免行的条件,也就是「先计数再修正」;那句话读起来像是「直接计数即可」。要不要改成带条件的表述我听你的,不改也能用,只是按字面算会偏。

  8. heptaspirit commented on Sep 24, 2026

    @heptaspirit
    Collaborator

    @modusensus

    补一条关于这批信号用途边界的看法,免得它被当成「减少库体积」的主力。

    内容哈希这条量的是穿透,不是粒度。 它在「同一个事实被再写一遍」时命中;而一族碎片里更多的是同一件事的不同侧面、或不同时间的口径,那些行归一化后并不相同。所以它对注入面的又散又多基本没有作用。归档回收同理:它清的是磁盘,归档行本来就不进注入。

    真正减少注入面行数的是升格聚合。 一族被吸收归档之后只剩指针行,注入池跟着降。那属于 #231 的整理动作,不在这批里。

    第二阶段打开时,提示里该带什么。 你定的机制是超预算返回原因、由 AI 显式二次触发才落笔(memory_save({confirm:true})),并且提示必须把「需要裁决的矛盾」写成合法理由。补一个同类的东西:同话题的重复命中可以在提示里带上「已有的那一条」,让 AI 有机会选择并入它而不是新写。这样闸门仍然只做成本判定、选择权在 AI,不做内容判定这条底线不动。要不要做、什么时候做,等 G2 的遥测有分布再说。

  9. added a commit that references this issue on Sep 24, 2026
  10. 8 remaining items

  11. heptaspirit commented on Oct 2, 2026

    @heptaspirit
    Collaborator

    A2 判据模块写完了:PR #354。

    按你 09-28 那两条约束落的:

    1. 审计位没动。 命中仍落 metadata.deny.reason='sensitive' 加 kind / label,enforced 由 decision 推出;feat(admission): 第 1 级确定性拒绝——密钥/PII 与空白噪声硬拒(#254) #332 里留的 sensitiveScan 注入点现在接到了真判据,write-admission.js 的决策形状一行没改。
    2. 12 条负样本原样跑真判据(不是 referenceScan)。test/sensitive-scan.test.js 第一条用例就是把 test/write-admission-samples.js 那 26 条整个跑一遍,正样本不漏、负样本一条不误杀。referenceScan 没删,它现在只服务闸门那组测;按你说的可以删,留给你或者和下一批一起收。

    判据自己的闸是顶层键 sensitiveScanEnabled,默认关;开着也只是仅告警,真拦仍要 writeAdmission.enabled 加 enforce。

    有一处要你拍板:我没把 A2 挂在 writeAdmission.enabled 底下,而是独立了一个键。 两批判据(空白噪声 vs 密钥 PII)的误杀面差一个量级,绑在同一个开关上就没法单独看 A2 的命中分布,而 kind 落审计的用途正是先看分布。你要是更想要「打开写入准入就两批一起跑」的形态,说一句我折成 writeAdmission.enabled 的子行为,能省一个键。

    顺带说一个探针里挖出来的真漏放:AWS 的 secret access key 两条规则都够不着。它没有前缀,通用那条又要求 secret 后面紧跟赋值号,多词键 AWS_SECRET_ACCESS_KEY= 不满足。补了专用的键名规则(40 位下限),并把「赋值型那条不看值的形状」一起锁进测试。

    第二批(autoSummarize 与 dream 输出那两个出口)没做,所以 PR 里没写 Closes #254,等你认领或指派。

    全量:1512 tests / 1511 pass / 0 fail / 1 skip。

  12. modusensus commented on Oct 20, 2026

    @modusensus
    MemberAuthor

    现状(2026-10-21 对照 main 实测)

    写入准入已实现四批,但 issue 正文的五个验收项一个都没勾——而且其中三项已经满足。

    已落地

    验收项 状态 证据
    默认关:零行为变化 ✅ config.js writeAdmission: z.object({ enabled: z.boolean().default(false), enforce: z.boolean().default(false) });settings.js:196-197 白名单成对注册
    被拒写入有审计与明确原因 ✅ write-admission.js:349 status: "skipped";模块头 :48 写明审计口径复用 llm_audit_logs(trigger_source=组件名、operation_type=…),reason 是稳定键
    密钥/隐私样例不漏放 ✅ sensitive-scan.test.js 16 个用例;判据来自 #164 A2(8dc2a59 feat(admission): 写入边界的密钥/PII 判据)
    与 #164 A2 的关系 部分 write-admission.js:2 记着「#164 的『第三腿触发』」口径,52dee2f 是第 1 级确定性拒绝(密钥/PII 与空白噪声硬拒)
    新配置键成对 + 旗标计数锁同步 ✅ settings.js:196-197 两键成对;test/settings.test.js 有白名单计数锁

    落地批次:7fd9be7(只计量不拦截)→ 52dee2f(第 1 级硬拒)→ 029dd5a(内容哈希计量 + 归档行纳入去重候选)→ 8dc2a59(A2 判据)→ 0e45a61(复审:启动期审计清理读装配后的 cfg)。测试:write-admission.test.js 35 个用例 + sensitive-scan.test.js 16 个。

    真正还缺的

    • 第 2 级的阈值:write-admission.js 头部注释写明「G1/G2 的阈值继续只计量,等遥测有分布再单独提」——即近重复/矛盾检测的阈值故意留空,等真实分布。
    • issue 正文提到的「去重键命中不进第 1 级」已在 config.js:726 附近按 2026-09-22 的判定归到 write-update。

    还缺什么

    1. 回填上面三项已满足的勾选框;
    2. 把「G1/G2 阈值待遥测」写成独立跟踪项(现在藏在代码注释里,外部看不见);
    3. 若第 2 级短期内不做,issue 可以按「第 1 级已交付,第 2 级另立」拆分关闭。

    接手建议

    代码不用动。这条现在最该做的是把正文与实现对齐——否则看起来像从没开工。

  13. modusensus commented on Nov 9, 2026

    @modusensus
    MemberAuthor

    正文已与实现对齐(2026-11-10):五个验收项勾掉(证据写在各项后面),补了落地记录与「还剩什么」两节。

    开单理由还剩两条,都收进「还剩什么」节:

    1. G1/G2 阈值——维持 9-22 与 10-21 两轮的口径:只计量不拍数,等 llm_audit_logs 攒出几百活跃会话量级的分布后,把分布摘出来贴在本帖,再提第二阶段(阈值 + memory_save({confirm:true}));
    2. A2 覆盖面(feat(admission): 写入边界的密钥/PII 判据(#164 A2 → #254 第 1 级) #354 评论区提的「第二批」)——autoSummarize 与 dream 输出两个出口未接入判据,共享包导入与 bootstrap 这类无 sessionKey 的写入同样不在覆盖内。要不要立项、什么时候接,在 feat(admission): 写入边界的密钥/PII 判据(#164 A2 → #254 第 1 级) #354 那条口径的基础上拍。

    第 2 级(近重复/矛盾检测)短期内不做的判断不变,本帖保持开放等分布数据;若想收干净,也可以按「第 1 级已交付」拆分——拆不拆看下一步排期。

  14. modusensus commented on Nov 11, 2026

    @modusensus
    MemberAuthor

    第 1 级已交付,五个验收项 2026-11-10 已与实现对齐(见上一条评论),按「第 1 级已交付」拆分收口:

    第 2 级(近重复 / 矛盾检测)短期不做的判断不变,不开 issue。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions