3 Commits
Author SHA1 Message Date
AskaEth 6a3f3a329a feat(memory): dedupe_group 候选动作 + 留存闸组合口径与 N6/N7 时点规则(M34)
M27 体检(DevDocs/audit/记忆库价值体检-2026-10-24.md)两条根治项:

1) 新增 `dedupe_group` 候选动作(不放大 containNone):
   - 簇只认「同 scope+kind+完全相同 tag 集」或「同 scope+kind+前 12 字相同」
     (后者附加词项余弦 ≥0.70 与同 kind 收窄);
   - 每簇保留超集:内容最长 > merge_count 大 > created_at 早 > id;
   - 安全门与既有合并路径同源:受限来源/绝对时刻/时效状态/低置信一律不进簇,
     数值与否定与存活行不一致的成员被逐条剔出;临时层行(expires_at 非空)
     两个方向都不进簇(存活行被清扫会给被吸收行留悬空 merged_into);
   - 已被 archive/merge 候选覆盖的条目不重复出第二条待办(抑制);
   - 落库形态与 merge 同构:source_ids 多条 + target_id,应用为整簇原子的软合并,
     回滚复用 revertTidyMerge(逐条复原);
   - 批量应用按**行**计上限 200 行/批(maxTidyApplyRowsPerBatch),超限如实 truncated;
   - 不进自动档(人工/批量点);试算(DryRun)零写库给出簇数与可减少行数。

2) 留存闸改组合口径(治根)+ N6/N7:
   - 护栏②从「含习惯/约定词就一票放行」改为「锚点词 **且** 含具体约束
     (数字/禁忌/频次规律)」;无约束的锚点交给更窄的规则判;
   - N1/N3/N4/N5 这几条词项规则改为「无锚点时才开火」(`主人喜欢说晚安`
     这类偏好不再因寒暄词被判临时);
   - 新增 N6(指示性时点,句首前 12 字内)与 N7(进行中状态),安全阀:习惯词、
     硬性长期事实词、数值/约束、寒暄/情绪词一律按回去;
   - 命中即第五态 defer,`expires_at = 判定时刻 + tempTtlSec`(默认 7 天,
     已落在 $DATA_DIR/config/memory-tidy.json,零新 .env 键);**只开新增侧**,
     存量一条不补写(用例钉住)。

契约(加性,不改既有字段语义):tidy dry-run/run 回执加 dedupeGroups /
dedupeRowsReducible / dedupeTruncated;批量应用回执加 deduped / rowsAffected。

实测(生产库只读 mode=ro,active 3667):
- 闸:改前 2 条(0.05%) → 改后 89 条(2.43%):n6 58 / n7 29 / n1 2;
  89 条里 30 条含锚点词,逐条人读均为「约定明天中午见」「约定下周留个午后」
  这类到期即失效的安排(唯一可议:1 条把「中午互动的日常」一起带进临时层)。
- dedupe_group:281 簇 / 可减 594 行(16.2% of active);tag_set 265 / prefix12 16;
  簇大小 2:170 3:48 4:27 5:10 6:10 7:7 8:2 9:2 10:1。

验收:go build ./... + go vet ./... + go test ./... 全绿。
2026-10-25 00:08:46 +08:00
AskaEth 6e28b38b08 feat(memory): 护栏抬高+时间预算闸+试算加性异步 与 D3 入库前审查(耦合两片一起落)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
【MT-A:护栏 + 诚实截断 + 试算异步】
- maxTidyCompareItems 400(写死)→2000 并开放为 MEMORY_TIDY_COMPARE_LIMIT;
  MEMORY_TIDY_CANDIDATE_LIMIT 默认 20→200(语义不变,仍每分区)
- 依据是首跑实测:459 条的分区被 400 截断 ⇒ compared(705) < scanned(784),
  候选数(77)由上限而不是规则决定;三处上限的 WARN 都补齐了总数/上限/分区名
- 新增「上限 + 时间预算」双闸(实测 2000 条/分区 ≈234s > 3 分钟墙钟):
  整分区跳过 / 两两比对中途收手,一律如实记 (budget) + WARN
- ?async=1 ⇒ 202 {runId,status};GET …/{runId} 轮询(writeAuth 同档);
  done 的 result 与同步 data 逐字节同形;并发=复用既有 runId、TTL 30min 自完成起算、
  重启即如实 404 tidy_run_not_found;不带 async 时同步路径逐字不变

【MT-B:D3 入库前审查(design/23 §15.1)】
- 新记忆经 Store.Add(仓库里唯一 INSERT INTO memory_items)落库后,由独立记忆管理
  会话(sess-memorytidy)在有界后台队列异步判定 skip/merge/modify/create
- 软合并回写(status + merged_into/merged_from/merge_count,可 revert、绝不改 content)
- 隔离来源(群聊/陌生人)**概不审查**、召回/超时失败**零改写**(fail-safe 结构性成立)
- 写入侧仅 +62.7µs/条(非阻塞入队);默认 ENABLED=true(主人裁定:入库前先审查)

【耦合说明(重要)】两片同包且互相依赖:MT-B 的 preinsert_recall.go 用 MT-A 的
collectTidyPairs 新签名 + tidyBudget ⇒ **各自单独都 build 不过**,故一并落地。
MT-A 为让整包编译,最小幅度适配了 MT-B 的 preinsert_recall.go / preinsert_test.go
(零值预算=行为不变;符号重名改名)。

【我的验收】隔离 worktree(37 文件)build/vet/linux/darwin 全过,切片包全绿;
我亲手打红两个变异:① 去掉 (compare) 截断标记 ⇒ TestTidyCompareLimit_HitIsLoud 红;
② 中和隔离来源守卫 ⇒ TestReviewRestrictedSourceIsNeverTouched 红。
唯一红是 internal/runtime/tool 的两条超时用例(该包无人改)⇒ 主仓库 -count=8 全绿,
判定为负载敏感 flake(当时两个整树验收 + 子代理并发),已记入开发习惯与约定 §7。

.env.example 补 MEMORY_REVIEW_* 四键(MT-B 无该文件权限,由我按其代码语义补写)。
2026-10-06 08:04:12 +08:00
AskaEth d6ed9cfa2b feat(memory): 记忆整理「试算(dry-run)」入口 + 面板卡(契约 v0.75)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
主人要「现在就想看到在 736 条上它会建议合并哪些」,而不是等 23:00 的低峰首跑。
- 后端:Tidier.DryRun + TidyService.DryRun,**复用同一条扫描→召回→判定→构造候选路径**(抽出 tidyRoundPolicy,生产轮与试算轮只差「候选出口」与「召回降级策略」两处;禁止第二套实现)
- 🔴 零副作用:不写 memory_tidy_candidates / memory_tidy_runs,绝不改 memory_items 任何列;机械证明 = 跑前跑后 cyrene.db 与 -wal 的 SHA256 一致(测试断言「表行数不变」)
- 失败码诚实:500 tidy_unavailable / 409 tidy_disabled / 500 tidy_recall_unavailable(**embedding 不可用不降级成 0 条**)/ 503 tidy_timeout / 500 tidy_dry_run_failed;**空数组唯一含义 = 算过了没建议**
- 端点 POST /api/v1/admin/memories/tidy/dry-run(writeAuth);面板「记忆」页加试算卡(点了才跑、无轮询、三态分开、明写「仅试算,不会改动任何记忆」)
- 真机 e2e(741 条活库副本):76 ms / 15 分区 / 53 候选 / 41 skipped / 1 vetoed(low_confidence);样例 merge reason=fact_subset score=1.0000、link reason=similar_but_not_contained score=0.9248
- 变异:dry-run 顺手写库 ⇒ 两条测试红(行数 0→2);自造阈值 ⇒ 红;失败回 200 空数组 ⇒ 红
- 如实登记:Compared=688 被 400/分区护栏截断、候选上限 20/分区被打满 ⇒ 护栏值需在 741 条量级重定(非 bug)
2026-10-05 22:06:54 +08:00