memory: 修注入开销、idf 排序与抽取诊断

注入侧
- context 在每次模型请求都触发,改为按 session + 最新用户消息去重:
  一个 turn 里 n 次工具调用不再重复注入 n 个 memory 块
- 默认注入从 1200 字符 / 内联 4 条正文,降到 600 字符 / 内联 0 条:
  大纲已说明存在什么,正文交给 memory_recall 按需取一次,避免每次
  请求都为同一批正文付费
- halfLifeDays 此前被静默丢弃,注入始终按 14 天半衰期排序,现已接通
- 召回缓存 2s -> 500ms,同一工具链里 remember -> recall 能读到刚写的文件

排序
- idf 原先拿「单文件匹配数」当分子、「总文件数」当分母,每个文件得到
  相同 idf,排序退化为纯词频;改为按文档频率计算的真实 idf

抽取
- 移除 400 段上限:它会静默截断长对话,300 轮只贡献前 ~50 轮
- role 在顶层或 message 下两种返回结构都读,之前只读 .role 得到空转录,
  长度门槛不过,抽取看起来永远不触发
- 去重改按关键词重叠计分,不再用 bigram 重叠:模型改写与人工措辞几乎
  不共享 bigram,真实改写只拿到 0.43;关键词是双方被迫共用的词汇
- 新增 memory_extract_dryrun:对真实模型跑完整抽取链路,不必改
  minTurns 或等长会话
- 新增 memory_inspect:诊断记录按 scope 存入 storage key,并保留历史
  与 fires/injected 计数,去重是否生效可被证伪

其他
- Context 改从 Plugin 命名空间导入,此前类型退化为 unknown
- store 暴露 kind 字段;compileRootTables 内联进 compileRoot
- frontmatter 列表元素统一 String(),index 不再出现数字

验证:由仓库所有者端到端验证(真实模型调用、真实会话);提交者未复跑,
以上为 diff 复核结论。
This commit is contained in:
2026-10-12 18:10:40 +08:00
parent 8dc02b6b35
commit cb9db666ba
5 changed files with 702 additions and 220 deletions
+17 -24
View File
@@ -45,8 +45,10 @@ export interface MemoryFile {
readonly title: string
/** One-line description shown in indexes and injected summaries. */
readonly summary: string
/** The "重点" column in index.md. */
/** Highlight column in index.md. */
readonly highlight: string
/** `kind` from frontmatter, e.g. "fact", "rule", "rule-proposal". */
readonly kind?: string
/** Scenarios that should pull this file in. Drives ROOT's routing table. */
readonly loadWhen: string[]
readonly pinned: boolean
@@ -170,6 +172,7 @@ function readMemoryFile(root: MemoryRoot, abs: string, category: string, name: s
title: String(data.title ?? firstHeading(body) ?? slug),
summary: String(data.summary ?? deriveSummary(body, slug)),
highlight: String(data.highlight ?? ""),
kind: data.kind === undefined ? undefined : String(data.kind),
loadWhen: toStringArray(data.load_when ?? data.loadWhen),
pinned: data.pinned === true || data.pinned === "true",
created: toNumber(data.created) ?? stat.mtimeMs,
@@ -431,12 +434,21 @@ function pruneEmptyIndexes(root: MemoryRoot, files: MemoryFile[]): void {
}
}
export interface CompiledRoot {
registry: string
routing: string
function describeCategory(category: string, files: MemoryFile[]): string {
const pinned = files.find((f) => isHardRule(f))
if (pinned && category === HARD_RULES_CATEGORY) return "不可违反的硬规则,常驻"
const first = files[0]
if (first?.fm.category_desc) return String(first.fm.category_desc)
return first ? first.summary.slice(0, 40) : "(空)"
}
export function compileRootTables(root: MemoryRoot, files: MemoryFile[]): CompiledRoot {
/** Recompile ROOT.md's generated regions and write it if it does not exist. */
export function compileRoot(root: MemoryRoot, files: MemoryFile[]): string {
// Same reason as the category index: ROOT.md may be the first thing written
// into a root that does not exist yet.
fs.mkdirSync(root.dir, { recursive: true })
const target = path.join(root.dir, "ROOT.md")
const categories = groupByCategory(files)
const registryRows = categories.map(({ category, files: list }) => {
@@ -461,25 +473,6 @@ export function compileRootTables(root: MemoryRoot, files: MemoryFile[]): Compil
? `\n| 场景 / 触发 | 读哪个文件 | 类型 |\n|---|---|---|\n${routingRows.join("\n")}\n`
: `\n_暂无 load_when 标记的记忆。给文件加 frontmatter 的 \`load_when:\` 即可自动进入路由表。_\n`
return { registry, routing }
}
function describeCategory(category: string, files: MemoryFile[]): string {
const pinned = files.find((f) => isHardRule(f))
if (pinned && category === HARD_RULES_CATEGORY) return "不可违反的硬规则,常驻"
const first = files[0]
if (first?.fm.category_desc) return String(first.fm.category_desc)
return first ? first.summary.slice(0, 40) : "(空)"
}
/** Recompile ROOT.md's generated regions and write it if it does not exist. */
export function compileRoot(root: MemoryRoot, files: MemoryFile[]): string {
// Same reason as the category index: ROOT.md may be the first thing written
// into a root that does not exist yet.
fs.mkdirSync(root.dir, { recursive: true })
const target = path.join(root.dir, "ROOT.md")
const { registry, routing } = compileRootTables(root, files)
let existing: string | undefined
try {
existing = fs.readFileSync(target, "utf8")