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
1 change: 1 addition & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- **standalone API `GET /context`(issue #370,#363 承诺)**:把宿主注入管线开放为端点——Mneme-Bridge 这类网页端桥接可以删掉自建的 pins 启发式了。`GET /context?q&topK&threshold&agent_scope&workspace_scope` 返回 `injectCandidates` 全语义(优先级分层 / 编码门控 / heat / scope 软加权 / hybrid 语义路 / pin 前置)+ 画像 + 规则的结构化聚合 `{profile, rules, items, pinnedCount}`,不含渲染文本(语言 / 时间前缀 / 热记忆是宿主会话概念,调用方自渲染)。与宿主注入的两点 HTTP 面差异:查询向量即时嵌入——首次调用即走语义路,不等异步 prefetch(宿主注入渲染必须同步,inject.js Bug4);不做跨轮轮换(daemon 无会话状态,调用方自管)。`threshold` 是 importance 阈值(默认 3,与 `/search` 的相似度 threshold 同名不同义);scope 两参全缺 = 不解析(软加权与 strictScope 硬过滤都不触发,与宿主会话身份取不到时同构),任一给出则缺维按 fail-closed。recall_runs 记账随 `injectCandidates` 内建(#217 曝光型访问事件,`mode='inject'` 与检索命中同表分账)——宿主注入同样记,端点自动同口径。`/search` 顺手补 scope 透传(`searchMemories` 本就收参、8790 此前没有入口;不传参数行为逐字节不变);`createStandaloneApi` 新增可选 `embedder` 注入(宿主与 `dsh-mneme-serve` 各传 semantic 装配的同一实例),缺省时 `/context` 降级规则 + BM25 档,嵌入失败同理绝不 500。宿主外部访问与 daemon 同享该路由;测试锁形状(四键)、空 q 规则档、threshold 语义、pin 前置、scope 硬过滤、嵌入接线与失败降级,daemon 侧全链路含 recall_runs 分账锁。
- **daemon 向量检索(PR2,#363)**:`dsh-mneme-serve` 的 `/search` 接入完整语义管线——embedder/reranker 装配与 boot 自动回填从 `index.js` **纯搬移**至 `src/semantic.js`(宿主与 daemon 共用同一份,调用时序契约原样;`backfillMissingEmbeddings` 经 index.js barrel 再出口,测试调用方零改动),daemon 侧新增 `createVectorIndex` 接线与 `--embed` 参数:`local`(默认,自管 runtime/嵌入模型缺失时经 `provisionRuntime` download 档自动取件,可用 `DSH_MNEME_RUNTIME_TARBALL_DIR`/`DSH_MNEME_RUNTIME_MIRROR` 换离线/镜像来源;失败降级关键词并打可操作日志)、`ollama`、`openai`(读宿主面板 vector-config)、`off`。向量轴有注入假 embedder 的回归锁;`createServeRuntime` 因此转为 async、语义键默认值在 `daemonSemanticCfg` 逐键锚定 config.js。
- **独立服务 daemon(`dsh-mneme-serve`,#363)**:mneme 现在能在 DSH 宿主之外常驻——`src/serve.js` 的 `createServeRuntime` 用最小装配(store → settings → mirror → service → maintenance → standalone API,每步锚定 index.js 装配行号)把数据面跑成独立进程,第三方集成(网页端桥接等)不必为挂载记忆库而保持 DSH 开机。第一期刻意无 LLM:巩固(autoDream)与蒸馏结构性不在 daemon 内,这是与宿主「单写者」的机械保证,不靠用户自觉。token 与 DSH 面板/CLI 共用同一 kv 凭证,端口/主机解析链与外部访问一致;`createStandaloneApi` 新增 `strictPort` 选项——daemon 的配置端口被占即报错退出而非顺延(第三方把 URL 写死,静默换端口等于坏),不传该选项的宿主旁路行为不变。`/search` 照常落 recall_runs,第三方检索的复用统计不缺数。多进程共存(daemon 与宿主同库互写互读)有专门回归锁;已知限制(双进程去重竞态、镜像双写、版本偏斜)见 docs/DAEMON.md。
- **`injectHeatEnabled`(issue #218 / E5 效用考卷,opt-in 默认关)**:注入侧 heat 拆出独立开关——E5 实测 heat 乘进注入排序在真实年龄混合下饿死老约束(现行量级 ≡ 拟合参数,importance-only 遵从 +12.7pp),默认关后 `heatEnabled` 开启时注入乘数恒 1(与 heat 关闭逐字节一致),显式开启恢复旧序;召回侧时钟与 sleep 降级联判不受影响。

## 🧹 工程

Expand Down
2 changes: 1 addition & 1 deletion dsh-mneme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ dsh web
| `bm25SearchEnabled` | `true` | BM25 稀疏第三路召回(v0.5.0):ASCII 词元 + CJK bigram,IDF 加权,散词/ID/代码片段查询不再依赖子串命中 |
| `adaptiveThresholdEnabled` | `true` | 自适应相似度阈值(v0.5.0):按查询形态动态截断(前缀 0.5 / 短查询 0.7 / 长查询 0.6 / 头部分差大放宽 0.5),显式传 `threshold` 走旧行为 |
| `hotMemoryEnabled` | `true` | 会话级短期热记忆总开关(v0.5.0):关闭后热记忆块不再注入(长期召回不受影响) |
| `heatEnabled` | `false` | 热度模型总开关(v0.7.0 / v0.7.20 回归,**默认关**——v0.7.12 起用户已习惯无 heat 行为):开启后提供热度字段 / sleep 热联合降级保护 / 前端热度投影,并在注入排序的优先级层内乘热度(#218;`order=chrono` 分页序与召回融合序不动);关闭则跳过 heat 计算与热度触达,sleep 降级退回纯时间分层。也走 feature_flags 白名单(面板可启停=回滚开关),lightMode 预设强制关 |
| `heatEnabled` | `false` | 热度模型总开关(v0.7.0 / v0.7.20 回归,**默认关**——v0.7.12 起用户已习惯无 heat 行为):开启后提供热度字段 / sleep 热联合降级保护 / 前端热度投影;注入排序乘热度已拆为独立开关 `injectHeatEnabled`(#218 / E5 考卷:heat 乘进注入排序在真实年龄混合下饿死老约束,默认关,显式开启恢复旧序)。关闭则跳过 heat 计算与热度触达,sleep 降级退回纯时间分层。也走 feature_flags 白名单(面板可启停=回滚开关),lightMode 预设强制关 |
| `heatGlobalBeta` | `1.0` | 广义指数形状参数 β(`H=exp(-λ·Δt^β)`,issue #218 拍板):β=1 纯指数;以 Δt>1 小时为准,β<1 衰减更慢(亚线性长尾)、β>1 衰减更快(超线性);0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小) |
| `heatTypeDecay` | 内置 TYPE_DECAY | per-type 衰减因子 λ;λ=0 的类型免疫(preference/pattern/summary 热度恒 1.0,sleep 永不降级) |
| `sleepHeatThreshold` | `0.05` | sleep 降级联合判定热度下限:heat<该值 **且** importance<5 才允许降级 |
Expand Down
5 changes: 3 additions & 2 deletions dsh-mneme/docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|---|---|---|---|
| `autoSummarize` | `true` | 会话/回合结束自动蒸馏记忆 | 关 = 只剩工具与命令写入口 |
| `autoSummarize` | `true` | 会话/回合结束自动蒸馏记忆。E10 EdgeMem 考卷(#339)实测:对「未来才问的具体值」类细节,零 LLM 原样写入保真 100% 而蒸馏丢 17/120——蒸馏的归纳收益在别的口径,二者是并存关系而非替代(原样写入并存的产品化见讨论 #361) | 关 = 只剩工具与命令写入口 |
| `summarizeProvider` / `summarizeModel` | 空 | 蒸馏专用模型路由 | 两者都非空才生效;空 = 用会话当前模型 |
| `distillMaxChars` | `24000` | 喂给蒸馏 LLM 的完整转录上限(字符,1000–200000) | 调大更完整、更贵 |
| `summarizeMinIntervalMinutes` | `0` | 同会话两次蒸馏的最小间隔(分钟,≤10080) | 0 = 不限(#127 节流三键之一) |
Expand Down Expand Up @@ -203,7 +203,8 @@

| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|---|---|---|---|
| `heatEnabled` **light** | `false` | 热度曲线(遗忘):热度字段 / sleep 降级联判 / 注入优先级层乘 heat(#218) | opt-in;关 = 注入乘数恒 1、sleep 退回纯时间分层 |
| `heatEnabled` **light** | `false` | 热度曲线(遗忘):热度字段 / sleep 降级联判(#218) | opt-in;关 = sleep 退回纯时间分层 |
| `injectHeatEnabled` | `false` | 注入排序乘 heat(#218 / E5 考卷:heat 乘进注入排序在真实年龄混合下饿死老约束,拆独立开关默认关) | 依赖 `heatEnabled`;关 = 注入乘数恒 1(与 heat 关闭逐字节一致);开 = 恢复层内乘 heat 旧序 |
| `heatGlobalBeta` | `1.0` | 广义指数形状参数 β(0.5–2) | 专家调优项,不进面板白名单 |
| `heatTypeDecay` | 内置默认 | per-type 衰减 λ(键为 type 字符串) | λ=0 的类型免疫(热度恒 1、sleep 永不降级) |

Expand Down
7 changes: 7 additions & 0 deletions dsh-mneme/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,13 @@ export const Config = z.object({
// 恒 1,sleep 降级退回纯时间分层。
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
heatEnabled: z.boolean().default(false),
// issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
// (现行量级 ≡ 拟合参数,注入集逐条相同;importance-only 遵从 +12.7pp),
// 故注入侧 heat 改为独立 opt-in:默认关 = heatEnabled 开启后热度字段/sleep
// 保护/前端投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致);
// 显式开启才恢复「层内乘 heat」的旧序。召回侧时钟(touchLastAccess)与
// sleep 降级联判不受本键影响——它们不参与注入排序。
injectHeatEnabled: z.boolean().default(false),
// 广义指数形状参数 β(heat = exp(-λ·Δt^β),issue #218 拍板):β=1 纯指数。
// 快慢以 Δt>1 小时为准——β<1 衰减更慢(亚线性长尾)、β>1 更快(超线性);
// 0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小)。专家调优项,不进面板白名单。
Expand Down
6 changes: 5 additions & 1 deletion dsh-mneme/lib/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,11 @@ export function createService({ store, mirror, config, onWrite, logger, document
// 合序都不动)。heat 关闭时不建表、不算 heat,权重恒 1,排序与改动前一致。
// selectiveInject 相似度重排(下方)开启且向量可用时相似度优先——heat 只
// 生效于规则路排序与语义路回填序。
const heatMap = config?.heatEnabled === true
// issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
// (现行量级 ≡ 拟合参数,importance-only 遵从 +12.7pp),故注入侧 heat 拆出
// 独立开关 injectHeatEnabled(默认关):不开时权重恒 1,排序与 heat 关闭
// 逐字节一致;召回侧时钟(touchLastAccess)与 sleep 降级联判不受影响。
const heatMap = config?.heatEnabled === true && config?.injectHeatEnabled === true
? new Map(filtered.map((m) => [m.id, computeHeat(m, Date.now(), config)]))
: null;
const heatOf = (m) => (heatMap ? heatMap.get(m.id) ?? 1 : 1);
Expand Down
2 changes: 2 additions & 0 deletions dsh-mneme/lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ const FEATURE_FLAG_BOOLEANS = [
"autoDreamFailureBackoff",
"sleepModeEnabled",
"heatEnabled",
// issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
"injectHeatEnabled",
"hybridInject",
"selectiveInjectEnabled",
"searchSemanticDedup",
Expand Down
7 changes: 7 additions & 0 deletions dsh-mneme/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,13 @@ export const Config = z.object({
// 恒 1,sleep 降级退回纯时间分层。
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
heatEnabled: z.boolean().default(false),
// issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
// (现行量级 ≡ 拟合参数,注入集逐条相同;importance-only 遵从 +12.7pp),
// 故注入侧 heat 改为独立 opt-in:默认关 = heatEnabled 开启后热度字段/sleep
// 保护/前端投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致);
// 显式开启才恢复「层内乘 heat」的旧序。召回侧时钟(touchLastAccess)与
// sleep 降级联判不受本键影响——它们不参与注入排序。
injectHeatEnabled: z.boolean().default(false),
// 广义指数形状参数 β(heat = exp(-λ·Δt^β),issue #218 拍板):β=1 纯指数。
// 快慢以 Δt>1 小时为准——β<1 衰减更慢(亚线性长尾)、β>1 更快(超线性);
// 0<Δt<1 的首小时内方向相反(Δt^β 随 β 增大而变小)。专家调优项,不进面板白名单。
Expand Down
6 changes: 5 additions & 1 deletion dsh-mneme/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,11 @@ export function createService({ store, mirror, config, onWrite, logger, document
// 合序都不动)。heat 关闭时不建表、不算 heat,权重恒 1,排序与改动前一致。
// selectiveInject 相似度重排(下方)开启且向量可用时相似度优先——heat 只
// 生效于规则路排序与语义路回填序。
const heatMap = config?.heatEnabled === true
// issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
// (现行量级 ≡ 拟合参数,importance-only 遵从 +12.7pp),故注入侧 heat 拆出
// 独立开关 injectHeatEnabled(默认关):不开时权重恒 1,排序与 heat 关闭
// 逐字节一致;召回侧时钟(touchLastAccess)与 sleep 降级联判不受影响。
const heatMap = config?.heatEnabled === true && config?.injectHeatEnabled === true
? new Map(filtered.map((m) => [m.id, computeHeat(m, Date.now(), config)]))
: null;
const heatOf = (m) => (heatMap ? heatMap.get(m.id) ?? 1 : 1);
Expand Down
2 changes: 2 additions & 0 deletions dsh-mneme/src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ const FEATURE_FLAG_BOOLEANS = [
"autoDreamFailureBackoff",
"sleepModeEnabled",
"heatEnabled",
// issue #218 / E5:注入侧 heat 独立开关(默认关,老用户 injectHeatEnabled=true 回滚)。
"injectHeatEnabled",
"hybridInject",
"selectiveInjectEnabled",
"searchSemanticDedup",
Expand Down
4 changes: 3 additions & 1 deletion dsh-mneme/test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,11 +674,13 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
// 块2 新增 graphWeightEnabled/graphWeightDelta、块3 新增 graphInjectHint/graphInjectBudget、
// 块4 新增 graphPassiveConfirm、
// issue #339 新增 dreamMergeGuard、
// issue #218/E5 新增 injectHeatEnabled、
// issue #164 A2 新增 sensitiveScanEnabled(顶层扁平键,走 configFlagValue 默认分支))
assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 3 + 2 + 2 + 1 + 1 + 1);
assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 3 + 2 + 2 + 1 + 1 + 1 + 1);
assert.equal(data.effective.dreamSkipInvalid, true);
assert.equal(data.effective.allowCrossTypeMerge, false);
assert.equal(data.effective.dreamMergeGuard, false);
assert.equal(data.effective.injectHeatEnabled, false);
assert.equal(data.effective.dreamMinIntervalMinutes, 0);
assert.equal(data.effective.autoDreamFailureBackoff, false);
assert.equal(data.effective.dreamMaxTokens, 131072);
Expand Down
46 changes: 31 additions & 15 deletions dsh-mneme/test/inject-heat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import { createService } from "../src/service.js";
// #218 v1:heat 作为注入排序的**同级内乘数**(issue #218 拍板口径)。
// 规则路 comparator 的层内权重 importance×quality 乘 heat;priority 分层
// (summary=0 / preference=1 / coding 同级 / 其余 2)与 store 的 order=chrono
// 分页序不动。heatEnabled=false(默认)时权重恒 1,排序与改动前一致——
// 本文件的关闭用例与 heat.test.js 的曲线断言共同锁住验收第 1 条。
// 分页序不动。
//
// issue #218 / E5 效用考卷:heat 乘进注入排序在真实年龄混合下饿死老约束
// (现行量级 ≡ 拟合参数,importance-only 遵从 +12.7pp),注入侧 heat 拆出
// 独立开关 injectHeatEnabled(默认关)——默认关档下 heatEnabled=true 的库
// 注入排序与 heat 关闭逐字节一致(平价锁);显式开启才恢复旧序。
// 召回侧时钟(touchLastAccess)与 sleep 降级联判不受本键影响。

const HOUR = 3600000;

Expand All @@ -19,40 +24,51 @@ function setup(config = {}) {

const titles = (candidates) => candidates.map((c) => c.title);

test("heat off(默认):层内维持 importance 序,与改动前一致", () => {
const { store, service } = setup();
// 三个用例共用的「冷高重要性 + 新低重要性」库。
function seedColdAndFresh(service) {
const cold = service.saveWithDedupe({ type: "decision", title: "冷的高重要性决策", content: "旧", importance: 4 }).memory;
service.saveWithDedupe({ type: "decision", title: "新的低重要性决策", content: "新", importance: 3 });
service.touchLastAccess(cold.id, new Date(Date.now() - 3000 * HOUR).toISOString());
// 新记忆不触达:ref 退 created_at(≈现在),heat≈1,不影响本用例
}

test("heat off(默认):层内维持 importance 序,与改动前一致", () => {
const { store, service } = setup();
seedColdAndFresh(service);
const picked = service.injectCandidates({ maxItems: 5, threshold: 3 });
assert.deepEqual(titles(picked), ["冷的高重要性决策", "新的低重要性决策"]);
store.close();
});

test("heat on:同层内新鲜低重要性记忆越过冷的高重要性记忆", () => {
test("heat on 且 injectHeatEnabled 关(新默认):注入序与 heat 关闭逐字节一致(E5 平价锁)", () => {
const { store, service } = setup({ heatEnabled: true });
const cold = service.saveWithDedupe({ type: "decision", title: "冷的高重要性决策", content: "旧", importance: 4 }).memory;
service.saveWithDedupe({ type: "decision", title: "新的低重要性决策", content: "新", importance: 3 });
service.touchLastAccess(cold.id, new Date(Date.now() - 3000 * HOUR).toISOString());
seedColdAndFresh(service);
const picked = service.injectCandidates({ maxItems: 5, threshold: 3 });
// E5 效用考卷:heat 乘进注入排序饿死老约束——默认不再乘,冷的高重要性决策
// 回到它 importance 应在的位置(与 heat off 用例逐条相同)。
assert.deepEqual(titles(picked), ["冷的高重要性决策", "新的低重要性决策"]);
store.close();
});

test("heat on 且 injectHeatEnabled 开:恢复旧序(老用户回滚口)", () => {
const { store, service } = setup({ heatEnabled: true, injectHeatEnabled: true });
seedColdAndFresh(service);
const picked = service.injectCandidates({ maxItems: 5, threshold: 3 });
// λ=0.002、3000h → 冷记忆权重 4×e^-6≈0.01,新鲜记忆 3×≈1 → 反超
assert.deepEqual(titles(picked), ["新的低重要性决策", "冷的高重要性决策"]);
store.close();
});

test("heat on 且 λ=0(免疫类型):乘数恒 1,importance 序不受影响", () => {
const { store, service } = setup({ heatEnabled: true, heatTypeDecay: { decision: 0 } });
const cold = service.saveWithDedupe({ type: "decision", title: "冷的高重要性决策", content: "旧", importance: 4 }).memory;
service.saveWithDedupe({ type: "decision", title: "新的低重要性决策", content: "新", importance: 3 });
service.touchLastAccess(cold.id, new Date(Date.now() - 3000 * HOUR).toISOString());
test("heat on 且 injectHeatEnabled 开 且 λ=0(免疫类型):乘数恒 1,importance 序不受影响", () => {
const { store, service } = setup({ heatEnabled: true, injectHeatEnabled: true, heatTypeDecay: { decision: 0 } });
seedColdAndFresh(service);
const picked = service.injectCandidates({ maxItems: 5, threshold: 3 });
assert.deepEqual(titles(picked), ["冷的高重要性决策", "新的低重要性决策"]);
store.close();
});

test("heat 不跨层:再热的 decision 也不越过 summary 层", () => {
const { store, service } = setup({ heatEnabled: true });
test("heat 不跨层:再热的 decision 也不越过 summary 层(injectHeatEnabled 开)", () => {
const { store, service } = setup({ heatEnabled: true, injectHeatEnabled: true });
service.saveWithDedupe({ type: "summary", title: "会话总览", content: "总览", importance: 2 });
service.saveWithDedupe({ type: "decision", title: "滚烫决策", content: "新", importance: 3 });
const picked = service.injectCandidates({ maxItems: 5, threshold: 3 });
Expand Down
Loading