Repository navigation
Description
背景
#249 的 P1 要把「约束/偏好类记忆 pin 住、逐字保真、不参与相关性竞争」落地,但注入侧现在只有「类型推断」:constraint / preference 这个类型本身就等于 pin,于是这些类型的每一条都自动具备「每轮必进」的资格,而预算只有 5(pinnedInjectBudget)。池子本身过大,稳定偏好只能靠相关性去挤那 10 个名额,挤不进去也无声。
这条线把 pin 从「类型推断」改成「用户亲手钉的显式事实」,顺带修掉三处同链缺陷。默认 pinnedInjectBudget = 0,预算关时注入序列与改前逐条相同,本线不改变任何人的现有行为。
缺陷 1(bug):pin 池被向量路合并的封顶挤出候选池
pin 筛选发生在 hybrid 合并之后,而 merged 已按 poolSize 封顶(生产 maxItems=5 → poolSize=10,向量路一次正好返回 10 条即 break)。规则窗口里的 constraint/preference 因此一条都进不了 candidates,pin 池只能从那 10 条里碰运气选,与「pin 不占 maxItems 名额」的既有承诺不一致。
修法:合并块之后、topic 重排之前,把被 poolSize 挤掉的 pin 类行补到候选池尾部;只在预算 > 0 时补,只补尾部,strictScope 与 scopeState 两道闸门照旧作用其上。
真库副本实测(6733 条有向量行、同一 query、1024 维、pinnedInjectBudget=5、maxItems=5):
| 臂 | pin 注入 | 注入总条数 |
|---|---|---|
| 改前 | 2/5 | 7 |
| 改后 | 5/5 | 10 |
无向量路径改前改后都 5/5、10 条且逐条同序 —— 补丁不改本来正常的路径。
缺陷 2(bug):生效判据写成 countPinned() > 0,会让 pin 全灭且永不回落
pin 行会在 memories 那边被归档、遗忘、硬删,而 pinned_memories 对这三种一无所知。只要表里还剩一行(哪怕早已归档),判据就一直判 explicit,于是 eligible 恒为空 —— pin 全灭,且永不回落到旧的类型口径,无日志无报错。
四场景复现:正常 pin 不触发;setArchived 触发;setForget 触发;remove 触发。前两种最要紧,因为归档/遗忘是日常操作,比硬删常见得多 —— 所以只补 DELETE 堵不住,根因在判据。
改法:判据从「表里有几行」改成「有几条能用的」。pinnedStats.source 因此有三档:explicit / fallback-none-usable(钉过但一条都用不了,静默降级必须留痕)/ type-fallback(从没钉过,升级即等价)。
缺陷 3(bug):memory_pin 的 injectable 回执漏了「暂停作用域」
描述文案、README 与 render 文案三处都写着「遗忘 / 归档 / 暂停作用域都优先于 pin,回执如实报告」,但 execute 只算了两项:
const injectable = !target.forgotten && !target.archived;被按下作用域(enforce 档)的行因此报 injectable: true 而实际一条都没注入。三组对照:按下暂停 → 回执 true、注入 0 条;不按暂停 → 回执 true、注入 1 条;暂停中钉、解除后重钉 → 两次都 true。报 true 的两种处境里一种真会注入、一种被挡住,回执分不出来。
改法:回执改用 service.filterPaused([target])(即 scopeStateGate.filter),与注入侧共用同一个出口,以后改闸不会漏掉回执这一侧。注入侧本身没坏 —— scopeStateGate 一直在拦,坏的是回执另写了一份判据。
落点
src/store.js:新增pinned_memories(memory_id PRIMARY KEY, created_at, note)+idx_pinned_created。独立表而非主表布尔列 —— 回退只需一条 DROP,不碰主表 31 列;pin 是「用户与记忆之间的一条关系」,created_at还能给出 pin 顺序。幂等靠CREATE TABLE IF NOT EXISTS,不登记applied_migrations(本表没有回填步骤)。外泄面为零:mirror / share 都不 SELECT 本表。src/service.js:取数按 id 点查(pin 行可能不在 200 行候选窗口内),带列裁剪、不SELECT *;两道闸不放宽。src/tools.js:memory_pin工具。
验收
- 存量库升级后新表为空 → 回落旧类型口径,两臂在真库副本上逐条 id 相同(
maxItems5 与 10 都验过) - 预算 0 时任何路径都不改注入序列
- 归档 / 遗忘 / 硬删三种场景各有用例
- 向量路填满
poolSize时 pin 仍拿满预算 - 暂停作用域下回执如实为 false
-
npm test全量 2858 用例 / 2857 通过 / 0 失败 / 1 跳过(src↔lib一致性守门通过)
不做
PINNED_MEMORY_TYPES 保持原样不动 —— 它在 write-admission 还承担写入配额豁免语义,与注入侧是两个不同的事,本线只把注入侧解耦。
Activity
modusensus commentedon Nov 6, 2026
复核结论(PR #593,head
8f9d8a95)已做源码级复核 + 独立复跑,并对 CodeRabbit 的 7 条行内评审逐条读源码核实。
三处缺陷与本 PR 的对应
src/service.js:1887-1921补池块在合并之后、topic 重排(:1927)之前countPinned() > 0在归档/遗忘/硬删后恒判 explicit、eligible 恒空pinnedStats.source三档explicit/fallback-none-usable/type-fallbackmemory_pin的injectable漏 scopeStateGate 的 enforce 档service.filterPaused([target]),与注入侧同一出口次序已核验:补池(:1887-1921)在 topic 重排(:1927)、strictScope(:1969)、
scopeStateGate.gateInjection(:1976)之前,pin 不是绕闸通道。CodeRabbit 7 条逐条核实
成立、建议修(2 条)
src/service.js:2789— 归档 evidence 时只按类型保护(src/document.js:297的pinnedTypes.has(row.type)),不查pinned_memories表。显式 pin 一条pattern/decision后,注册文档吸收它时仍会被store.setArchived(id, true)归档,之后注入跳过它——用户没取消 pin。这是本 PR 把 pin 从类型推断改成显式存储后新出现的缺口,属本 PR 应修未修。修法:src/document.js:297追加|| store.isPinned?.(row.id)。src/tools.js:502—injectable没把pinnedInjectBudget === 0算进去。pinnedInjectBudget默认 0(src/service.js:1882),此时整段补池不执行、pin 完全不起作用,但回执照样报injectable: true。回执在说谎,建议加上pinnedBudget > 0条件或在 render 里补一句说明。不成立(4 条)
src/service.js:1899子系统开关 —eligible是pinRows.filter((m) => live.has(m.id) ...),live来自已按injectTypes/allowNarrative过滤的 candidates,关掉的子系统行进不了live。src/service.js:1996graphHint — 已实现m.graphHint !== true(:1996、:1916、:1998 三处)。src/store.js:3939id trim — 工具层传的是target.id(store 铸造的真实主键),不是用户原始输入。src/store.js:3971SQLite 变量上限 — 理论上限,pin 行数以个位计且 :1900 有catch { pinRows = [] }回落;附带那条「SQL 注入」告警是误报(${placeholders}只由?, ?拼成,${memoryColumnList}来自 PRAGMA 白名单,实际值全部绑定)。成立但既有缺陷、被本 PR 新调用点暴露(1 条)
src/tools.js:536—strictScope=true且传入不存在的 id 时,visibleUnderStrictScope(:124-128)无undefined短路,service.isVisibleInScope(:130-143)直接读m.agent_scope_source→ TypeError 而非 "memory not found"。该缺陷在 origin/main 已有(memory_get同样走它),本 PR 新增memory_pin又多一个触发面。修法一行:src/tools.js:125加if (memory === undefined) return true;,惠及memory_get/update/delete/pin。schema 变更(需要用户拍板)
本 PR 新增
CREATE TABLE IF NOT EXISTS pinned_memories (memory_id PRIMARY KEY, created_at, note)+idx_pinned_created,幂等靠CREATE TABLE IF NOT EXISTS、不登记applied_migrations。独立表而非主表布尔列,回退是一条DROP。按长期纪律「线上库迁移压到考后」,这条需要用户本人确认是否例外放行。复跑(本机实测)
scripts/check-sync.jsnpm test环境 Windows 11 + Node 24.15.0;未跑 ubuntu / node22 / test-host-legacy(由 CI 覆盖)。CI:6 条 test 腿 + gitleaks + dependency-review + workflow-hygiene + zizmor + CodeRabbit + codecov/patch 全 success,osv-scanner skipping。codecov patch 97.64%,7 行未覆盖。
未做
未在真实记忆库上跑 pin 的端到端注入对比(用的是测试夹具);未做面板真机走查;未验证
document类型被显式 pin 后与documentMemoryEnabled的交互。