Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dsh-mneme/CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions dsh-mneme/docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
| `summarizeDedupeMode` | `off` | 落库前去重档位:`off` / `title`(拦完全同名)/ `vector`(同会话语义近邻) | `vector` 复用已有 embedding 列,无 LLM 调用 |
| `summarizeDedupeMinSim` | `0.92` | `vector` 档并入阈值(0.5–0.99) | 仅 `vector` 档生效 |
| `summarizeDedupeWindowHours` | `24` | `vector` 档回看窗口(小时,≤168) | 仅 `vector` 档生效 |
| `summarizeBatchCrossTypeDedupe` | `true` | 批内跨类型去重(#687):同一窗蒸馏产出里,把重复的 `pitfall`/`constraint` 或 `history`/`project` 两组类型对合成一条 | **默认开**(2026-11-13 拍板;设 `false` 即关,行为与加这个开关之前逐字节一致);只认这两组类型对(其余类型对实测有 20% 会丢信息)。处置是 `supersede`:被取代的一条**正文零改写**、不归档,只是不再默认注入——判错也不丢内容 |
| `summarizeBatchCrossTypeMinSim` | `0.85` | 批内跨类型去重的相似度阈(0.5–0.99) | 仅上一项开启时生效;与质量过滤的 duplicate 信号同档,避免同一条「是不是同一件事」出现第二个数 |
| `distillRateLimitIntervalMs` | `1000` | 蒸馏请求全局串行排队的相邻间隔(毫秒) | 0 = 关闭排队(默认开的 429 保护随之失效) |
| `distillRateLimitRetries` | `3` | 命中 429 的指数退避重试次数(≤10) | 重试全程对用户透明 |
| `distillRateLimitBaseDelayMs` | `1000` | 429 退避基数(毫秒,100–60000) | 1s→2s→4s… |
Expand Down
8 changes: 6 additions & 2 deletions dsh-mneme/docs/ENTITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ CREATE TABLE IF NOT EXISTS entities (
type TEXT, -- person|project|concept|technology|organization
first_seen TEXT NOT NULL, -- 首次出现时间
last_seen TEXT NOT NULL, -- 最近出现时间
mention_count INTEGER DEFAULT 1, -- 提及次数(同名去重后递增)
canonical_memory_id TEXT -- 规范来源记忆(保留位)
mention_count INTEGER DEFAULT 1 -- 提及次数(同名去重后递增)
);
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
-- #688 写入侧归一:命中键在 SQL 里折叠(lower + trim),索引与查询同源。
-- 必须带 type 维度——只按 name 折叠会把 95 组跨类型同名实体误并成一个
-- (如 DeepSeek 这个技术 与 deepseek 这个组织);也不剥版本号后缀,
-- DeepSeek-V3 与 V4 是两个对象。存量行不改写,只在写入/查找时归一。
CREATE INDEX IF NOT EXISTS idx_entities_name_key ON entities(lower(trim(name)), type);
```

### `entity_attrs` — 属性(快照式时间轴)
Expand Down
11 changes: 11 additions & 0 deletions dsh-mneme/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,10 @@ window.__ModuleLoader__.load({
"memory.features.codingBoostFactor": "编码记忆加权",
"memory.features.memoryQualityFilter.enabled": "质量过滤",
"memory.features.memoryQualityFilter.enabled.hint": "低价值记忆自动归档、注入时降权。",
// #687:写侧去重,与质量过滤相邻——两条都在问「这一条该不该成为一条记忆」。
// 文案必须说清「原文不删」:被取代方只是不再默认注入,用户不会丢内容。
"memory.features.summarizeBatchCrossTypeDedupe": "批内跨类型去重",
"memory.features.summarizeBatchCrossTypeDedupe.hint": "同一轮蒸馏里把重复的「踩坑/约束」或「历史/项目」只留信息更全的一条;被取代的一条原文保留、不再默认注入。",
"memory.features.llmAudit.enabled": "调用审计",
"memory.features.llmAudit.enabled.hint": "后台模型调用与 token 消耗都有账可查。",
// #254:写入准入两层。检测关时整条路径不碰;检测开、拦截关 = 仅告警 +
Expand Down Expand Up @@ -2192,6 +2196,10 @@ window.__ModuleLoader__.load({
"memory.features.codingBoostFactor": "Coding boost factor",
"memory.features.memoryQualityFilter.enabled": "Quality filter",
"memory.features.memoryQualityFilter.enabled.hint": "Low-value memories get archived and down-ranked.",
// #687 — write-side dedupe. The hint must say the loser keeps its text:
// supersede only stops default injection, it does not delete content.
"memory.features.summarizeBatchCrossTypeDedupe": "Batch cross-type dedupe",
"memory.features.summarizeBatchCrossTypeDedupe.hint": "Within one distillation run, keeps only the fuller of a duplicated pitfall/constraint or history/project pair; the superseded row keeps its text but stops being injected by default.",
"memory.features.llmAudit.enabled": "Call audit",
"memory.features.llmAudit.enabled.hint": "Background model calls and token spend are on record.",
// #254 — two layers: enabled runs the check (audit only), enforce actually
Expand Down Expand Up @@ -4719,6 +4727,9 @@ window.__ModuleLoader__.load({
// 蒸馏与写入:写进去的东西长什么样——实体抽取(子块:路由与思考强度)、
// 编码回溯、文档摘要卡、质量过滤、原文留存(子块:天数)。
{ key: "group.pipeline", items: ["entityExtractionEnabled", "codingRetrospect", "documentMemoryEnabled", "memoryQualityFilter.enabled",
// #687 批内跨类型去重:写侧行为,紧贴质量过滤(同一件事的两道闸——
// 质量过滤判「值不值得记」,这条判「同一轮里有没有记两遍」)。
"summarizeBatchCrossTypeDedupe",
// #399 的留存总闸放组末:distillSub(天数行)经 subAfter 紧贴它渲染。
"distillRecords.enabled", "imageDescribeEnabled"] },
// 检索与注入:捞什么、排什么序——热记忆、时间前缀、重排、BM25、语义去重、
Expand Down
11 changes: 11 additions & 0 deletions dsh-mneme/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export const Config = z.object({
summarizeDedupeMode: z.union([z.const("off"), z.const("title"), z.const("vector")]).default("off"),
summarizeDedupeMinSim: z.number().min(0.5).max(0.99).default(0.92),
summarizeDedupeWindowHours: z.natural().min(0).max(168).default(24),
// #687 批内跨类型去重(默认开;2026-11-13 桉桉拍板,明知这是行为变化)。同一窗蒸馏
// 里,模型偶尔把一个结论同时写成 pitfall 与 constraint(或 history 与 project)——
// 类型不同,summarizeDedupeMode 的 type+title 去重看不见它们。这个闸只认这两组类型对,
// 命中走 supersede(被取代方正文零改写、不 merge):判错时原文仍在库里,只是不再默认
// 注入,显式搜索照常可达,继任者被归档时旧行还会自动恢复可见——所以风险不对称,
// 关掉它才是让 #687 报的双录继续产出。不想用时把这一项设 false 即可(面板同一格)。
// 不开常开类型闸的理由:2026-11-13 实测 45 对里 9 对(20%)合并会丢信息。
summarizeBatchCrossTypeDedupe: z.boolean().default(true),
// 批内比较的相似度阈(与 quality-filter 的 duplicate 信号同档 0.85:同一条
// 「是不是在说同一件事」不该有第二个数)。
summarizeBatchCrossTypeMinSim: z.number().min(0.5).max(0.99).default(0.85),
// 智能调速器(429 保护,默认开):蒸馏 LLM 调用全局串行排队,相邻请求
// 间隔 distillRateLimitIntervalMs(默认 1s 一次);命中 429 限流时按
// distillRateLimitBaseDelayMs 指数退避(1s→2s→4s…)自动重试
Expand Down
7 changes: 6 additions & 1 deletion dsh-mneme/lib/entities/extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,12 @@ function sanitizeExtractedData(data, config, logger = null) {
}

async function resolveEntity(entity, store) {
const existing = store.findEntityByName(entity.name);
// #688: the type rides the lookup. Name-only folding merges genuinely distinct
// objects — the measured library had 95 such groups ("DeepSeek" the technology
// next to "deepseek" the organization). Case/whitespace folding itself happens
// in store.findEntityByName; the extractor must not pre-normalize, or the two
// sides would fold with different rules (JS toLowerCase vs SQLite lower()).
const existing = store.findEntityByName(entity.name, entity.type);
if (existing) {
store.updateEntity(existing.id, {});
return existing.id;
Expand Down
Loading
Loading