@modusensus/dsh-mneme 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +15 -5
  2. package/lib/api-standalone.js +12 -0
  3. package/lib/api.js +52 -14
  4. package/lib/client.js +130 -8
  5. package/lib/config.js +8 -0
  6. package/lib/dream/decisions.js +55 -8
  7. package/lib/dream/sleep.js +79 -17
  8. package/lib/dream.js +61 -9
  9. package/lib/entities/extractor.js +2 -2
  10. package/lib/index.js +6 -0
  11. package/lib/lang.js +13 -2
  12. package/lib/local-embedder.js +9 -1
  13. package/lib/reranker.js +38 -8
  14. package/lib/scope.js +39 -4
  15. package/lib/service.js +121 -18
  16. package/lib/store.js +243 -40
  17. package/lib/summarize.js +2 -2
  18. package/lib/tools.js +64 -30
  19. package/lib/vector-index.js +2 -8
  20. package/lib/version-check.js +122 -0
  21. package/package.json +2 -1
  22. package/scripts/sync-test-badge.mjs +61 -0
  23. package/src/api-standalone.js +12 -0
  24. package/src/api.js +52 -14
  25. package/src/config.js +8 -0
  26. package/src/dream/decisions.js +55 -8
  27. package/src/dream/sleep.js +79 -17
  28. package/src/dream.js +61 -9
  29. package/src/entities/extractor.js +2 -2
  30. package/src/index.js +6 -0
  31. package/src/lang.js +13 -2
  32. package/src/local-embedder.js +9 -1
  33. package/src/reranker.js +38 -8
  34. package/src/scope.js +39 -4
  35. package/src/service.js +121 -18
  36. package/src/store.js +243 -40
  37. package/src/summarize.js +2 -2
  38. package/src/tools.js +64 -30
  39. package/src/vector-index.js +2 -8
  40. package/src/version-check.js +122 -0
  41. package/test/api.test.js +5 -0
  42. package/test/conflict-freeze.test.js +15 -5
  43. package/test/dream-archive-guardrail.test.js +116 -0
  44. package/test/dream-coverage-degrade.test.js +86 -0
  45. package/test/dream.test.js +15 -11
  46. package/test/inject-first-round-bm25.test.js +89 -0
  47. package/test/llm-message-source.test.js +177 -0
  48. package/test/reranker-single-logit.test.js +73 -0
  49. package/test/scope-candidates.test.js +239 -0
  50. package/test/scope-explicit.test.js +452 -0
  51. package/test/scope-storage.test.js +13 -5
  52. package/test/scope-strict.test.js +107 -41
  53. package/test/skip-invalid-restore.test.js +7 -5
  54. package/test/sleep-scheduler-rearm.test.js +89 -0
  55. package/test/sleep.test.js +2 -2
  56. package/test/standalone-api.test.js +34 -1
  57. package/test/store-vector-cache.test.js +98 -0
  58. package/test/tools-memory-get-schema.test.js +65 -0
  59. package/test/version-check.test.js +152 -0
package/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-988%20passed-success)](https://github.com/modusensus/dsh-mneme)
8
+ [![tests](https://img.shields.io/badge/tests-1061%20passed-success)](https://github.com/modusensus/dsh-mneme)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/modusensus/dsh-mneme/ci.yml)](https://github.com/modusensus/dsh-mneme/actions)
10
- [![node](https://img.shields.io/badge/node-24%2B-blue)](https://nodejs.org)
10
+ [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/@modusensus/dsh-mneme?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
12
12
  [![coverage](https://img.shields.io/codecov/c/github/modusensus/dsh-mneme/main)](https://codecov.io/gh/modusensus/dsh-mneme)
13
13
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  | ① 写入 | ② 存储 | ③ 进化 |
23
23
  |--------|--------|--------|
24
- | 对话中模型主动记录(`memory_save`,可带敏感度与事件时间);会话结束自动提炼(`autoSummarize`) | SQLite 主库 + 人类可编辑 Markdown 镜像;实体 / 属性 / 时间轴三层结构化;记忆带 agent / workspace / 敏感度 / 发生时间标注 | 新会话自动注入相关记忆(按当前作用域加权,strictScope 开启后硬隔离);autoDream 后台去重 / 合并 / 归档,冲突冻结待人工裁决 |
24
+ | 对话中模型主动记录(`memory_save`,可带敏感度与事件时间);会话结束自动提炼(`autoSummarize`) | SQLite 主库 + 人类可编辑 Markdown 镜像;实体 / 属性 / 时间轴三层结构化;记忆带 agent / workspace / 敏感度 / 发生时间标注 | 新会话自动注入相关记忆(按当前作用域加权,strictScope 开启后显式收窄的归属硬隔离);autoDream 后台去重 / 合并 / 归档,冲突冻结待人工裁决 |
25
25
 
26
26
  ```bash
27
27
  # 30 秒上手
@@ -228,6 +228,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
228
228
 
229
229
  | 版本 | 亮点 |
230
230
  |------|------|
231
+ | **v0.8.1** | 归属显式声明与人工纠偏 + 插件版本自检:记忆归属三段式可控——per-dim 来源列 + save/update scope 参数 + 面板可编辑(#170 第 1 步,#171);sleep 跨 scope 相似对停车进冲突队列人工裁决(第 2 步,#172);`strictScope` 硬过滤只认显式声明、自动标注降为纯软加权(第 3 步,#173,4.3 已确认);`GET /api/dsh-mneme/version-check` 只读路由 + 设置页偏差横幅(仅 outdated 渲染,registry 精确 host 白名单 + TTL 缓存 + 全失败静默,pnpm 钉子警示 + 市场收录延迟说明,#174 后续);1026 测试全绿 |
232
+ | **v0.8.2** | 生态与性能批次(社区贡献集中合入):LLM 消息补 source 契约——严格 provider 下 dream/summarize/sleep/实体抽取不再序列化抛错(#189/#190);memory_get 输出 schema 与共享 DTO 同源(#184/#186);sleep 计时器撞 CD 重排 + 构造挂表(#187/#192);单 logit 重排器恒 0.5 修复 + 镜像/量化接线(#188/#193);连通性探测改发 user 消息(#197);首轮注入 BM25 同步兜底——老偏好不再占满槽位(#198/#199);autoDream 宽容路径闭环——coverage 不足降级 degraded + archive 类型护栏/批量上限 `dreamMaxArchivePerRun`(#104/#200/#201);检索路径性能第一批——updated_at 索引 + all() 剪列 + 向量解析缓存(#202/#203,all() 231→51ms、searchVector ~140→16ms);standalone API 补 profile/rules 路由(#180/#185);1061 测试全绿 |
231
233
  | **v0.8.0** | 作用域隔离(issue #17 批次 A)+ 冲突集中处理 + 斜杠命令提交 Agent + 注入转义回归修复:记忆按 agent 与工作区双维标注(agent_scope / workspace_scope / sensitivity / occurred_at 四列可空),检索按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见),opt-in `strictScope` 硬过滤贯通检索/注入/列表/单取(fail-closed);`scopeEnabled` / `strictScope` 默认关、面板「作用域隔离」组可启停;去重键扩展含作用域三元——跨作用域同标题不再物理合并;`occurred_at` 事件发生时间 + `occurred_from/to` 过滤贯通搜索与列表;冲突冻结(conflictFreezeEnabled)新增人工出口——状态页冲突队列并排对比 + 保留 A/B/仅标记(#166);斜杠命令经 agent.followup 真正提交模型(#152);恢复 v0.7.4 的注入花括号转义 + hot memory 跳过 reasoning——`{{.Server.Version}}` 类文本不再卡死会话(#165);988 测试全绿 |
232
234
  | **v0.7.32** | 冲突动作集扩展 + 运行时自管化 + 记忆/提示语言 + 候选集 hybrid 档 + 一批修复:`sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)——supersede 赢家正文干净、输家归档附「已被取代」注记,differentiate 双留+差异注记;sleep 校验接入 `dreamSkipInvalid` 宽容策略(默认 true,一票否决变宽容,严格可关);运行时三档取件(收编/本地 .tgz/registry)+ transformers 转可选 peer(#131);`memory.language`(zh/en)提示/注入/镜像语言可选(#124);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)、#135 五连修(前缀解析/effort 最低档/importance 豁免/ready 语义/覆盖度降级告警)、boot 回填指纹短路修复(#128)、degraded 轮跳过明细落库(#104);916 测试全绿、总覆盖 92.7%(runtime 96%、embedding 100%) |
233
235
  | **v0.7.30** | 状态页向量卡修复(issue #118):改读开放的 `/semantic`——不再吃 `/vector-config` 的 401「加载失败」,ollama/local 模式不再恒显「未启用」;新增「初始化中(embedder 不可达,正在重试)」与「已索引 N / M 条」回填进度显示;legacy OpenAI 兼容 embedder「Object · 0D」显示修复(embedder 显式 `name: "OpenAI"` + 维度回退 `index.dimension`);「已索引 N / M」分子分母同口径(`embeddedCount` 剔除归档/遗忘,与 `count()` 默认过滤对齐);embedder init 失败有界重试(共 5 次,不再一次性永久降级,unload 清理定时器)+ `OllamaEmbedder.ready` 生命周期位 + `/semantic` 新增 `ready` 字段;745 测试全绿 |
@@ -335,6 +337,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
335
337
  | **v0.7.31** | ✅ 完成 | peerDependencies 宿主版本声明修复 | 原 `^0.1.0-rc.6` 按 node-semver 预发布元组规则不匹配 `0.1.5-rc.1` 等中间预发布版本(当前 dsh 用户安装 ERESOLVE),也违反 awesome-dsh-plugin 的 peer-range 预发布分支规范;改显式三段式预发布分支(覆盖 0.1.0-rc.6 至 0.1.5-rc.1 全部已发布 0.1.x 含预发布 + 0.2 预发布留显式分支 + 0.3+ 待验证后放开);745 测试全绿 |
336
338
  | **v0.7.32** | ✅ 完成 | sleep 冲突动作集扩展 + 运行时自管化 + 候选集 hybrid 档 + 一批修复 | `sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)+ sleep 校验接入 `dreamSkipInvalid` 宽容策略 + 被跳决策带 phase 落库 `dream_runs.skipped`;运行时三档取件 + transformers 转可选 peer(#131);`memory.language` 提示/注入/镜像语言可选(#124);候选集向量驱动 hybrid 档(#125,PR #147);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)+ #135 五连修 + boot 回填指纹短路修复(#128)+ degraded 轮跳过明细落库(#104);runtime 覆盖统计归一 + 补齐(总覆盖 92.7%);916 测试全绿 |
337
339
  | **v0.8.0** | ✅ 完成 | 作用域隔离 + 冲突队列 | 按 agent 与工作区双维隔离记忆(标注 / 检索加权 / opt-in strictScope 四路硬过滤,issue #17 批次 A);冻结冲突新增人工确认队列(状态页并排对比,#166);斜杠命令真正提交 Agent(#152);注入转义回归恢复 + hot memory 跳过 reasoning(#162)。图谱增强(兴趣漂移可视化)、跨 workspace 记忆共享仍在规划 |
340
+ | **v0.8.1** | ✅ 完成 | 归属显式声明与人工纠偏 + 插件版本自检 | 归属三段式可控:per-dim 来源列 + save/update scope 参数 + 面板可编辑(#170 第 1 步,#171);sleep 跨 scope 相似对停车人工裁决接冲突队列(第 2 步,#172);`strictScope` 硬过滤只认显式声明、自动标注降纯软加权(第 3 步,#173)。插件自报新版本:/version-check 只读路由 + 设置页 outdated 横幅(registry 精确白名单 + TTL 缓存 + 全失败静默,pnpm 钉子警示,#174 后续,#176);1026 测试全绿 |
341
+ | **v0.8.2** | ✅ 完成 | 生态与性能批次:宽容路径闭环 + 检索提速 + 社区贡献集中合入 | autoDream 宽容路径三方向闭环:coverage 不足降级 degraded、archive 类型护栏 + `dreamMaxArchivePerRun` 批量上限、失败审计明细贯通(#104/#200/#201);检索路径三处固定成本:updated_at 索引 + all() 剪列 + 向量解析缓存,all() 231→51ms、searchVector ~140→16ms(#202/#203);LLM 消息补 source 契约(#189/#190);首轮注入 BM25 同步兜底(#198/#199);memory_get schema 同源(#184/#186);sleep CD 重排(#187/#192);单 logit 重排器修复(#188/#193);standalone profile/rules 路由(#180/#185);1061 测试全绿 |
338
342
 
339
343
  > 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
340
344
 
@@ -411,6 +415,7 @@ dsh web
411
415
  | `dreamCandidateMode` | `window` | dream 候选集构造:`window`(只取最近 `dreamMaxSnapshotSize` 条)/ `hybrid`(在此基础上并入向量翻出的高相似组)。纯时间窗口下,实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口——该合并的一对几乎永远碰不到面。**已知边界**:dream 侧动作集仍是五分支(keep / merge / archive / update / conflict),hybrid 翻出的互补型 / 演进型对在 dream 里没有 `differentiate` / `supersede` 出口;正确出口在 sleep 侧(`sleepActionSet: full`)|
412
416
  | `dreamCandidateMax` | `0` | hybrid 的候选总量上限;`0` = 复用 `dreamMaxSnapshotSize`。候选总量与库总量解耦,输入成本不随库增长 |
413
417
  | `dreamCandidateMinSim` | `0.85` | hybrid 判「高相似」的阈值(与 sleep 的 normal 档对齐,两个模块共用同一个「高相似」定义) |
418
+ | `dreamMaxArchivePerRun` | `8` | 单轮 archive 决策上限(超限整单拒绝,防一次性大扫除;正常清理可调高) |
414
419
  | `apiToken` | 空 | 可选 API 鉴权 token;设置后写操作与密钥接口要求 `Authorization: Bearer <apiToken>` |
415
420
  | `embedProvider` | `openai` | 语义后端:`openai`(默认,兼容 v0.1)/ `local`(ONNX 离线)/ `ollama` |
416
421
  | `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地 ONNX embedding 模型 |
@@ -431,6 +436,7 @@ dsh web
431
436
  | `rerankBatchSize` | `8` | Rerank 批大小 |
432
437
  | `rerankMaxCandidates` | `30` | Rerank 最大候选数 |
433
438
  | `rerankScoreThreshold` | `0.1` | Rerank 分数阈值(低于丢弃) |
439
+ | `rerankDtype` | `q8` | Rerank 模型量化档(`q8` ≈ fp32 体积的 1/4,`fp32` 可关) |
434
440
  | `reflectionUpdateEnabled` | `true` | update 决策总开关 |
435
441
  | `reflectionFailureTracking` | `true` | 失败追踪总开关 |
436
442
  | `reflectionUpdateMaxPerRun` | `2` | 每次整理最多 update 数 |
@@ -461,13 +467,15 @@ dsh web
461
467
  | `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10, exemptImportance:4}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入;`exemptImportance` 豁免:importance ≥ 该值只降权不归档,#135) |
462
468
  | `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(autoDream/autoSummarize)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
463
469
  | `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL);去重键扩展含作用域三元——跨作用域同标题不再物理合并;检索按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见)。也走 feature_flags 白名单(面板可启停) |
464
- | `strictScope` | `false` | 作用域硬过滤(v0.8.0,依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,带他者作用域的记忆完全不可见;会话身份解析不到时 fail-closed 只见未标注行。关闭时为软隔离(降权保留可见)。也走 feature_flags 白名单 |
470
+ | `strictScope` | `false` | 作用域硬过滤(v0.8.0 引入,v0.8.1 起只认显式声明;依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,**显式声明**收窄到他者作用域的记忆完全不可见;载体自动标注只降权保留可见——真正的物理隔离请用 sensitivity。会话身份解析不到时 fail-closed 只挡显式行。关闭时全部为软隔离(降权保留可见)。也走 feature_flags 白名单 |
465
471
  | `conflictFreezeEnabled` | `false` | 冲突冻结(v0.4.4):dream 发现矛盾对不自动裁决,冻结进冲突队列;状态页「冲突队列」支持并排对比与人工确认(保留 A / 保留 B / 仅标记已处理,v0.8.0) |
466
472
  | `escapePromptVariables` | `true` | 注入文本花括号转义(v0.8.0 恢复,issue #162):注入边界把 `{{...}}` 转义,防止 hot memory / 记忆原文里的 Go template / Vue 语法触发宿主 interpolate 抛错卡死会话(v0.7.4 曾修复、v0.7.11 误删) |
467
473
 
468
474
  > 🔐 **API 安全**:DSH 无内置鉴权且默认仅监听 `127.0.0.1`。插件 API 默认开放(便于 Web 面板即装即用)。如需防护(如局域网暴露),在配置中设置 `apiToken`:写操作(画像/规则/命令)与密钥端点(`vector-config`、`vector-reindex`)需携带 `Authorization: Bearer <token>`(前端设置面板可填入同一 token),只读的 `list` / `search` / `semantic` 保持开放。`/api/dsh-mneme/vector-config` 返回的 `apiKey` 已掩码(`sk-***…`),存储仍保留明文供调用;前端回传空或掩码值表示"不改 key"。
469
475
 
470
476
 
477
+ > ⚠️ **已知边界:极简模式(minimal agent preset)下不注入**(宿主设计,非插件缺陷):minimal 预设的组合文件显式 `complete: true` + `includeRuntimeContext: false`——整条系统提示词被 persona 钉死、全部 runtime context 快照被压制,**记忆注入 / 用户画像 / hot memory 在极简模式会话中一律不送达模型**(宿主内置 context 同样消失)。判断与解法:会话头部显示「极简模式」即为该形态;需要记忆注入请改用标准模式(会话级切换,或 `~/.dsh/settings.yaml` 设 `agent-presets.default: standard`——注意 `--dump-config` 显示的是 bundle 默认值,会被用户层覆盖,不能反映实际生效值);极简模式下的过渡方案是把画像/规则写入 `AGENTS.md`(agent-instructions 走 section 路径,不受该压制影响)。
478
+
471
479
  ## 外部 API 与 CLI
472
480
 
473
481
  除 DSH 内部端口外,插件还可以开启一个**独立的 HTTP 外部 API**(默认 `http://127.0.0.1:8790`,Bearer token 鉴权),供其他插件、CLI 脚本或桌面工具读写记忆,不依赖 DSH 内部端口。
@@ -484,6 +492,8 @@ dsh web
484
492
  |------|------|------|
485
493
  | `GET` | `/health` | 健康检查(免鉴权),返回 `{ok:true}` |
486
494
  | `GET` | `/status` | 版本、记忆统计、实体数、运行时长 |
495
+ | `GET` | `/profile` | 用户画像,返回 `{profile:"..."}`;未设置时为空字符串 |
496
+ | `GET` | `/rules` | 行为规则列表,返回 `{rules:[...]}`;未设置时为空数组 |
487
497
  | `GET` | `/memories?limit&offset&type&minImportance&source&order=chrono` | 分页列出记忆 |
488
498
  | `GET` | `/memories/:id` | 单条记忆 |
489
499
  | `POST` | `/memories` | 新增记忆 `{type,title,content,importance?,tags?,source?}` |
@@ -590,7 +600,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
590
600
  ```bash
591
601
  cd dsh-mneme
592
602
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
593
- npm test # 运行 988 个测试
603
+ npm test # 运行 1026 个测试
594
604
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
595
605
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
596
606
  ```
@@ -152,6 +152,18 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
152
152
  return;
153
153
  }
154
154
 
155
+ // --- GET /profile: user self-description -------------------------------
156
+ if (req.method === "GET" && pathname === "/profile") {
157
+ sendJson(res, 200, { profile: settings?.getProfile?.() ?? "" });
158
+ return;
159
+ }
160
+
161
+ // --- GET /rules: agent behavior rules -----------------------------------
162
+ if (req.method === "GET" && pathname === "/rules") {
163
+ sendJson(res, 200, { rules: settings?.getRules?.() ?? [] });
164
+ return;
165
+ }
166
+
155
167
  // --- GET /memories: paged + filtered list (same semantics as the
156
168
  // internal /api/dsh-mneme/list) ------------------------------------
157
169
  if (req.method === "GET" && pathname === "/memories") {
package/lib/api.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { URL } from "node:url";
2
- import { readFileSync } from "node:fs";
3
2
  import { timingSafeEqual, randomBytes } from "node:crypto";
4
3
  import { FEATURE_FLAG_SPEC } from "./settings.js";
5
4
  import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
@@ -8,6 +7,7 @@ import { computeHeat } from "./heat.js";
8
7
  import { describeStreamFailure, resolveRoute } from "./dream.js";
9
8
  import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
10
9
  import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
10
+ import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
11
11
 
12
12
  // headers:少数端点(/export 附件下载)需要追加 Content-Disposition 等响应头。
13
13
  function sendJson(res, status, payload, headers = {}) {
@@ -24,16 +24,8 @@ function sendAttachment(res, status, contentType, filename, body) {
24
24
  res.end(body);
25
25
  }
26
26
 
27
- // 导出 JSON 的 version 字段:读插件根的 package.json(src/ 与 lib/ 都在根下
28
- // 一层,相对 import.meta.url 解析一致)。读取失败(打包/受限环境)降级为
29
- // "unknown",导出本身仍然可用。
30
- const PACKAGE_VERSION = (() => {
31
- try {
32
- return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "unknown";
33
- } catch {
34
- return "unknown";
35
- }
36
- })();
27
+ // 版本号统一从 version-check.js 取(同一份读 package.json 逻辑,读取失败
28
+ // 同样降级 "unknown")——/info 与 /version-check 两个路由共用,不再各写一份。
37
29
 
38
30
  // Defaults for the standalone external API — keep in step with the schema
39
31
  // defaults in config.js (externalApiPort / externalApiHost).
@@ -175,6 +167,29 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
175
167
  }
176
168
  });
177
169
 
170
+ // 版本自检(#174 后续):运行版本 vs npm registry latest。只读,宿主围栏
171
+ // 统一鉴权(与 dream-status 同款,面板 apiFetch 直接可用)。registry 查询
172
+ // 全失败时 latest=null、status=unknown——前端对 unknown 完全静默,版本提示
173
+ // 是锦上添花,绝不因它新增故障面。
174
+ register({
175
+ kind: "exact",
176
+ path: "/api/dsh-mneme/version-check",
177
+ handler(req, res) {
178
+ if (req.method !== "GET") {
179
+ sendJson(res, 404, { error: "not-found" });
180
+ return;
181
+ }
182
+ fetchLatestVersion()
183
+ .then((latest) => sendJson(res, 200, {
184
+ version: PACKAGE_VERSION,
185
+ latest,
186
+ status: classify(PACKAGE_VERSION, latest),
187
+ checkedAt: new Date().toISOString()
188
+ }))
189
+ .catch(() => sendJson(res, 200, { version: PACKAGE_VERSION, latest: null, status: "unknown" }));
190
+ }
191
+ });
192
+
178
193
  register({
179
194
  kind: "exact",
180
195
  path: "/api/dsh-mneme/list",
@@ -315,7 +330,17 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
315
330
  // 按钮无放大风险。
316
331
  maxTokens: 1024,
317
332
  ...(reasoningEffort && reasoningEffort !== "none" ? { reasoningEffort } : {}),
318
- messages: [{ role: "system", content: [{ type: "text", text: "Reply with exactly one word: ok" }] }]
333
+ // role 用 user 而非 system(#189 后续):0.1.6-alpha.1 起适配器把 system
334
+ // 抽成顶层参数,system-only 对话会被官方 API 以
335
+ // 「messages: at least one message is required」整单拒绝——探测按钮 502。
336
+ // user 消息在任何适配器下都是一条合法对话,探测从此不怕 host 演进。
337
+ messages: [
338
+ {
339
+ role: "user",
340
+ content: [{ type: "text", text: "Reply with exactly one word: ok" }],
341
+ source: { kind: "plugin", plugin: "dsh-mneme" }
342
+ }
343
+ ]
319
344
  })) {
320
345
  if (chunk.type === "text-delta" && typeof chunk.text === "string") reply += chunk.text;
321
346
  if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
@@ -480,6 +505,19 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
480
505
  }
481
506
  patch.tags = body.tags;
482
507
  }
508
+ // v0.8.1 底座(issue #170):显式 scope 修正。null=放宽到全局,
509
+ // 字符串=收窄/改标;归一化("global"/"*"/空白→NULL + explicit 章 +
510
+ // 审计行)在 service.updateMemory 内做,路由只做形状校验。
511
+ if (body.agent_scope !== undefined && body.agent_scope !== null && typeof body.agent_scope !== "string") {
512
+ sendJson(res, 400, { error: "invalid-agent-scope" });
513
+ return;
514
+ }
515
+ if (body.workspace_scope !== undefined && body.workspace_scope !== null && typeof body.workspace_scope !== "string") {
516
+ sendJson(res, 400, { error: "invalid-workspace-scope" });
517
+ return;
518
+ }
519
+ if (body.agent_scope !== undefined) patch.agent_scope = body.agent_scope;
520
+ if (body.workspace_scope !== undefined) patch.workspace_scope = body.workspace_scope;
483
521
  if (body.archived !== undefined && typeof body.archived !== "boolean") {
484
522
  sendJson(res, 400, { error: "invalid-archived" });
485
523
  return;
@@ -501,7 +539,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
501
539
  ...history
502
540
  ].slice(0, 20);
503
541
  }
504
- if (Object.keys(patch).length) service.update(id, patch);
542
+ if (Object.keys(patch).length) service.update(id, patch, { actor: "panel" });
505
543
  if (body.archived !== undefined) service.setArchived(id, body.archived);
506
544
  sendJson(res, 200, { memory: service.getById(id) });
507
545
  });
@@ -1270,7 +1308,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
1270
1308
  });
1271
1309
 
1272
1310
  return {
1273
- routes: 23,
1311
+ routes: 30,
1274
1312
  dispose: () => {
1275
1313
  for (const dispose of disposers) dispose();
1276
1314
  }
package/lib/client.js CHANGED
@@ -270,6 +270,8 @@ window.__ModuleLoader__.load({
270
270
  "memory.settings.feedback.email": "邮件反馈(work@modusensus.space)",
271
271
  "memory.settings.feedback.browse": "浏览仓库已知问题",
272
272
  "memory.settings.feedback.hint": "反馈前先搜搜是否已有相同问题,能省一份重复的 issue~",
273
+ "memory.settings.version.outdated": "🆕 有新版本 {v}(当前运行 {c})",
274
+ "memory.settings.version.outdatedHint": "若安装时指定过版本号,常规升级不会跨大/小版本(pnpm 钉子)——请重新安装或在升级命令加 --latest;npm 已发布而市场暂未收录属正常(约 1 天延迟)。",
273
275
  "memory.settings.mode.title": "运行模式",
274
276
  "memory.settings.mode.desc": "轻量模式只保留核心的记忆读写与自动注入(关闭 autoDream 巩固、实体抽取、语义搜索等高级功能),适合只想「记住偏好」的轻量使用;标准模式开启全部功能。",
275
277
  "memory.settings.mode.light": "轻量",
@@ -351,7 +353,7 @@ window.__ModuleLoader__.load({
351
353
  "memory.features.scopeEnabled": "作用域标注",
352
354
  "memory.features.scopeEnabled.hint": "按会话身份(agent/工作区)给新记忆打归属标注;重启 DSH 后生效",
353
355
  "memory.features.strictScope": "严格隔离",
354
- "memory.features.strictScope.hint": "检索与注入硬过滤:带他者作用域的记忆完全不可见(关闭时仅降权保留可见);重启 DSH 后生效",
356
+ "memory.features.strictScope.hint": "检索与注入硬过滤,只对显式声明的归属生效:显式收窄到他者作用域的记忆不可见;载体自动标注只降权保留可见(真正的物理隔离请用 sensitivity)。重启 DSH 后生效",
355
357
  "memory.features.trustEpistemicWeighting": "可信度加权",
356
358
  "memory.features.trustEpistemicWeighting.hint": "按来源可信度调整召回排序(实验性)",
357
359
  "memory.features.reflectionFailureTracking": "反思失败追踪",
@@ -407,6 +409,12 @@ window.__ModuleLoader__.load({
407
409
  "memory.explorer.scopeBadge": "作用域",
408
410
  "memory.explorer.detail.agentScope": "Agent 作用域",
409
411
  "memory.explorer.detail.workspaceScope": "工作区作用域",
412
+ "memory.explorer.detail.scopeGlobal": "全局(所有会话可见)",
413
+ "memory.explorer.detail.scopeSourceAuto": "自动",
414
+ "memory.explorer.detail.scopeSourceExplicit": "显式",
415
+ "memory.explorer.detail.scopeEditHint": "留空 = 全局;也可填写具体标签收窄归属",
416
+ "memory.explorer.detail.scopeWidenConfirm": "确认放宽可见性?",
417
+ "memory.explorer.detail.scopeWidenHint": "放宽后该记忆将对所有会话可见",
410
418
  "memory.explorer.detail.sensitivity": "敏感度",
411
419
  "memory.explorer.detail.occurred": "发生时间",
412
420
  "memory.explorer.detail.edit": "编辑",
@@ -611,6 +619,8 @@ window.__ModuleLoader__.load({
611
619
  "memory.settings.feedback.email": "Email feedback (work@modusensus.space)",
612
620
  "memory.settings.feedback.browse": "Browse known issues",
613
621
  "memory.settings.feedback.hint": "Search for an existing issue first — it saves a duplicate.",
622
+ "memory.settings.version.outdated": "🆕 New version {v} available (running {c})",
623
+ "memory.settings.version.outdatedHint": "If the plugin was installed with a pinned version, regular upgrades never cross minor/major lines — reinstall or pass --latest. A fresh npm release may take about a day to appear in the market.",
614
624
  "memory.settings.mode.title": "Runtime mode",
615
625
  "memory.settings.mode.desc": "Light mode keeps only the core memory read/write and auto-injection (autoDream consolidation, entity extraction and semantic search are off) — for light use where you just want preferences remembered. Standard mode enables everything.",
616
626
  "memory.settings.mode.light": "Light",
@@ -692,7 +702,7 @@ window.__ModuleLoader__.load({
692
702
  "memory.features.scopeEnabled": "Scope tagging",
693
703
  "memory.features.scopeEnabled.hint": "Stamp new memories with the writing session's identity (agent/workspace); takes effect after restarting DSH",
694
704
  "memory.features.strictScope": "Strict isolation",
695
- "memory.features.strictScope.hint": "Hard-filter search and injection: memories scoped to other agents/workspaces become invisible (off = demoted but visible); takes effect after restarting DSH",
705
+ "memory.features.strictScope.hint": "Hard-filter search and injection for EXPLICITLY declared scopes only: memories explicitly narrowed to other agents/workspaces become invisible; auto carrier labels are demoted but stay visible (use sensitivity for true physical isolation). Takes effect after restarting DSH",
696
706
  "memory.features.trustEpistemicWeighting": "Credibility weighting",
697
707
  "memory.features.trustEpistemicWeighting.hint": "Adjust recall ranking by source credibility (experimental)",
698
708
  "memory.features.reflectionFailureTracking": "Reflection failure tracking",
@@ -748,6 +758,12 @@ window.__ModuleLoader__.load({
748
758
  "memory.explorer.scopeBadge": "Scoped",
749
759
  "memory.explorer.detail.agentScope": "Agent scope",
750
760
  "memory.explorer.detail.workspaceScope": "Workspace scope",
761
+ "memory.explorer.detail.scopeGlobal": "Global (visible to all sessions)",
762
+ "memory.explorer.detail.scopeSourceAuto": "auto",
763
+ "memory.explorer.detail.scopeSourceExplicit": "explicit",
764
+ "memory.explorer.detail.scopeEditHint": "Empty = global; or enter a specific label to narrow the scope",
765
+ "memory.explorer.detail.scopeWidenConfirm": "Widen visibility?",
766
+ "memory.explorer.detail.scopeWidenHint": "Widening makes this memory visible to all sessions",
751
767
  "memory.explorer.detail.sensitivity": "Sensitivity",
752
768
  "memory.explorer.detail.occurred": "Occurred",
753
769
  "memory.explorer.detail.edit": "Edit",
@@ -2068,6 +2084,9 @@ window.__ModuleLoader__.load({
2068
2084
  const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
2069
2085
  // 反馈入口的插件版本(GET /info)。失败保持 "unknown",链接仍可用。
2070
2086
  const [pkgVersion, setPkgVersion] = react.useState("unknown");
2087
+ // 版本自检(#174 后续):运行版本 vs registry latest。仅 outdated 渲染
2088
+ // 横幅,其余状态(up-to-date / ahead / unknown / 查询失败)零渲染零打扰。
2089
+ const [updateInfo, setUpdateInfo] = react.useState(null);
2071
2090
  // 运行模式 — light vs standard; null = still loading. The card keeps
2072
2091
  // its own busy/saved/error state so it never blocks the others.
2073
2092
  const [mode, setMode] = react.useState(null);
@@ -2113,6 +2132,21 @@ window.__ModuleLoader__.load({
2113
2132
  return () => { cancelled = true; };
2114
2133
  }, []);
2115
2134
 
2135
+ // 版本自检:只读 /version-check(宿主围栏鉴权)。非 outdated 一律归
2136
+ // null——横幅不出现即是「无需处理」;接口失败也静默,不新增故障面。
2137
+ react.useEffect(() => {
2138
+ let cancelled = false;
2139
+ apiFetch("/api/dsh-mneme/version-check")
2140
+ .then((res) => (res.ok ? res.json() : null))
2141
+ .then((j) => {
2142
+ if (!cancelled && j && j.status === "outdated" && typeof j.latest === "string" && typeof j.version === "string") {
2143
+ setUpdateInfo({ latest: j.latest, current: j.version });
2144
+ }
2145
+ })
2146
+ .catch(() => {});
2147
+ return () => { cancelled = true; };
2148
+ }, []);
2149
+
2116
2150
  // 反馈链接预填:环境信息(插件版本 + 平台)+ 问题描述骨架。邮件主题带
2117
2151
  // 版本/平台方便归档,正文同款模板;纯链接零后端成本。
2118
2152
  const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
@@ -2323,6 +2357,17 @@ window.__ModuleLoader__.load({
2323
2357
  }
2324
2358
 
2325
2359
  return h("div", null,
2360
+ // 版本自检横幅 — 仅 outdated 时渲染(up-to-date/ahead/unknown/失败
2361
+ // 全部零渲染)。钉子警示:安装时指定过版本号的 profile 会被 pnpm
2362
+ // 挡住常规升级(#174 报障者的实际成因);市场收录新发布约有 1 天延迟。
2363
+ updateInfo && h("section", { className: "mneme-set-sec" },
2364
+ h("div", { className: "mneme-set-title" },
2365
+ t("memory.settings.version.outdated")
2366
+ .replace("{v}", updateInfo.latest)
2367
+ .replace("{c}", updateInfo.current)
2368
+ ),
2369
+ h("div", { className: "mneme-featsubhint" }, t("memory.settings.version.outdatedHint"))
2370
+ ),
2326
2371
  // 用户画像 — who the agent is talking to
2327
2372
  h("section", { className: "mneme-set-sec" },
2328
2373
  h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
@@ -3191,11 +3236,30 @@ window.__ModuleLoader__.load({
3191
3236
  // 右侧滑出:全文、来源、质量分、关联实体与手动操作(编辑/归档/删除)。
3192
3237
  // 编辑态本地暂存草稿,保存经 POST /update 落库;后端成功后重渲染镜像,
3193
3238
  // 前端只做本地视图同步。两步删除确认在此完成(红钮武装 → 实心红提交)。
3239
+ // v0.8.1 底座(issue #170):scope 行的来源徽注(显式/自动)。存量行无来源
3240
+ // 时不加后缀——与 A4 的「未标注零视觉变化」同款克制。
3241
+ const scopeSourceSuffix = (t, source) =>
3242
+ source === "explicit" ? ` · ${t("memory.explorer.detail.scopeSourceExplicit")}`
3243
+ : source === "auto" ? ` · ${t("memory.explorer.detail.scopeSourceAuto")}`
3244
+ : "";
3245
+ const scopeProvenanceTitle = (t, source, decidedAt) =>
3246
+ [
3247
+ source === "explicit" ? t("memory.explorer.detail.scopeSourceExplicit")
3248
+ : source === "auto" ? t("memory.explorer.detail.scopeSourceAuto") : null,
3249
+ decidedAt ?? null
3250
+ ].filter(Boolean).join(" · ");
3251
+
3194
3252
  function MemoryDrawer({ t, memory, conflict, deleting, deleteError, onClose, onDelete, onSaved }) {
3195
3253
  const [editing, setEditing] = useState(false);
3196
3254
  const [title, setTitle] = useState(memory.title || "");
3197
3255
  const [content, setContent] = useState(memory.content || "");
3198
3256
  const [importance, setImportance] = useState(memory.importance || 3);
3257
+ // v0.8.1 底座(issue #170):scope 归属的人工修正入口。空串=放宽到全局
3258
+ // (发送 null),非空=收窄到该标签;只有真的改动才会随 patch 发送并盖
3259
+ // explicit 章——普通编辑不得污染归属来源。
3260
+ const [agentScope, setAgentScope] = useState(memory.agent_scope || "");
3261
+ const [workspaceScope, setWorkspaceScope] = useState(memory.workspace_scope || "");
3262
+ const [confirmWiden, setConfirmWiden] = useState(false);
3199
3263
  const [confirmDelete, setConfirmDelete] = useState(false);
3200
3264
  const [busy, setBusy] = useState(false);
3201
3265
  const [savedTick, setSavedTick] = useState(false);
@@ -3234,7 +3298,25 @@ window.__ModuleLoader__.load({
3234
3298
  const save = () => {
3235
3299
  const t2 = title.trim();
3236
3300
  if (!t2) return;
3237
- postUpdate({ title: t2, content, importance }, {});
3301
+ const patch = { title: t2, content, importance };
3302
+ const curAgent = memory.agent_scope ?? "";
3303
+ const curWorkspace = memory.workspace_scope ?? "";
3304
+ const nextAgent = agentScope.trim();
3305
+ const nextWorkspace = workspaceScope.trim();
3306
+ const agentChanged = nextAgent !== curAgent;
3307
+ const workspaceChanged = nextWorkspace !== curWorkspace;
3308
+ if (agentChanged) patch.agent_scope = nextAgent || null;
3309
+ if (workspaceChanged) patch.workspace_scope = nextWorkspace || null;
3310
+ // 放宽可见性(标注→全局)必须显式确认(issue #170 的安全阀):
3311
+ // 第一次点保存只弹确认,再次点「确认放宽」才真正落库。
3312
+ const widening = (curAgent && agentChanged && !nextAgent)
3313
+ || (curWorkspace && workspaceChanged && !nextWorkspace);
3314
+ if (widening && !confirmWiden) {
3315
+ setConfirmWiden(true);
3316
+ return;
3317
+ }
3318
+ setConfirmWiden(false);
3319
+ postUpdate(patch, {});
3238
3320
  };
3239
3321
 
3240
3322
  return h("aside", { className: "mneme-drawer", "aria-label": t("memory.explorer.detail") },
@@ -3278,12 +3360,40 @@ window.__ModuleLoader__.load({
3278
3360
  memory.occurred_at && h(react.Fragment, null,
3279
3361
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.occurred")),
3280
3362
  h("span", { className: "mneme-dmetaval", title: memory.occurred_at }, formatDateShort(memory.occurred_at))),
3281
- memory.agent_scope && h(react.Fragment, null,
3363
+ // v0.8.1 底座:scope 行双态——查看态展示归属 + 来源徽注(显式声明的
3364
+ // 「全局」也渲染,不再因 NULL 而隐身);编辑态为文本框(留空=全局)。
3365
+ (editing || memory.agent_scope || memory.agent_scope_source === "explicit") && h(react.Fragment, null,
3282
3366
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.agentScope")),
3283
- h("span", { className: "mneme-dmetaval", title: memory.agent_scope }, memory.agent_scope)),
3284
- memory.workspace_scope && h(react.Fragment, null,
3367
+ editing
3368
+ ? h("input", {
3369
+ type: "text",
3370
+ className: "mneme-select",
3371
+ style: { height: 26, fontSize: 12, justifySelf: "start", width: "100%" },
3372
+ value: agentScope,
3373
+ placeholder: t("memory.explorer.detail.scopeGlobal"),
3374
+ title: t("memory.explorer.detail.scopeEditHint"),
3375
+ onChange: (e) => setAgentScope(e.target.value)
3376
+ })
3377
+ : h("span", {
3378
+ className: "mneme-dmetaval",
3379
+ title: scopeProvenanceTitle(t, memory.agent_scope_source, memory.scope_decided_at) || memory.agent_scope
3380
+ }, (memory.agent_scope ?? t("memory.explorer.detail.scopeGlobal")) + scopeSourceSuffix(t, memory.agent_scope_source))),
3381
+ (editing || memory.workspace_scope || memory.workspace_scope_source === "explicit") && h(react.Fragment, null,
3285
3382
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.workspaceScope")),
3286
- h("span", { className: "mneme-dmetaval", title: memory.workspace_scope }, memory.workspace_scope)),
3383
+ editing
3384
+ ? h("input", {
3385
+ type: "text",
3386
+ className: "mneme-select",
3387
+ style: { height: 26, fontSize: 12, justifySelf: "start", width: "100%" },
3388
+ value: workspaceScope,
3389
+ placeholder: t("memory.explorer.detail.scopeGlobal"),
3390
+ title: t("memory.explorer.detail.scopeEditHint"),
3391
+ onChange: (e) => setWorkspaceScope(e.target.value)
3392
+ })
3393
+ : h("span", {
3394
+ className: "mneme-dmetaval",
3395
+ title: scopeProvenanceTitle(t, memory.workspace_scope_source, memory.scope_decided_at) || memory.workspace_scope
3396
+ }, (memory.workspace_scope ?? t("memory.explorer.detail.scopeGlobal")) + scopeSourceSuffix(t, memory.workspace_scope_source))),
3287
3397
  memory.sensitivity && h(react.Fragment, null,
3288
3398
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.sensitivity")),
3289
3399
  h("span", { className: "mneme-dmetaval", title: memory.sensitivity }, memory.sensitivity)),
@@ -3316,7 +3426,16 @@ window.__ModuleLoader__.load({
3316
3426
  h("div", { className: "mneme-dactions" },
3317
3427
  editing
3318
3428
  ? h(react.Fragment, null,
3319
- h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: save }, t("memory.explorer.detail.save")),
3429
+ confirmWiden
3430
+ ? h(react.Fragment, null,
3431
+ h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-warn,#b8860b)" } },
3432
+ t("memory.explorer.detail.scopeWidenHint")),
3433
+ h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: busy, onClick: save },
3434
+ t("memory.explorer.detail.scopeWidenConfirm")),
3435
+ h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => setConfirmWiden(false) },
3436
+ t("memory.explorer.cancel"))
3437
+ )
3438
+ : h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: save }, t("memory.explorer.detail.save")),
3320
3439
  h("button", {
3321
3440
  type: "button",
3322
3441
  className: "mneme-btn",
@@ -3326,6 +3445,9 @@ window.__ModuleLoader__.load({
3326
3445
  setTitle(memory.title || "");
3327
3446
  setContent(memory.content || "");
3328
3447
  setImportance(memory.importance || 3);
3448
+ setAgentScope(memory.agent_scope || "");
3449
+ setWorkspaceScope(memory.workspace_scope || "");
3450
+ setConfirmWiden(false);
3329
3451
  }
3330
3452
  }, t("memory.explorer.detail.cancel")),
3331
3453
  savedTick && h("span", { className: "mneme-saved" }, t("memory.explorer.detail.saved")),
package/lib/config.js CHANGED
@@ -111,6 +111,10 @@ export const Config = z.object({
111
111
  // 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
112
112
  // 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
113
113
  dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
114
+ // Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
115
+ // 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
116
+ // 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
117
+ dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
114
118
  // Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
115
119
  // dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
116
120
  // 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
@@ -257,6 +261,10 @@ export const Config = z.object({
257
261
  rerankBatchSize: z.natural().min(1).max(64).default(8),
258
262
  rerankMaxCandidates: z.natural().min(5).max(100).default(30),
259
263
  rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
264
+ // #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
265
+ // fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
266
+ // 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
267
+ rerankDtype: z.string().default("q8"),
260
268
 
261
269
  // --- reflection: update decision + failure tracking (v0.2.1) ------------
262
270
  reflectionUpdateEnabled: z.boolean().default(true),
@@ -11,6 +11,15 @@ export const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update"
11
11
  // = preferred. observation (measured) > inferred (derived) > subjective (guess).
12
12
  const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
13
13
 
14
+ // Issue #104 方向 2:archive 类型护栏的长保留类型集合——这些类型的存在意义
15
+ // 就是长期保存(踩坑记录、约束、用户偏好、挖掘出的模式),LLM 以「价值判断」
16
+ // 类理由批量归档它们时误伤损失最大。conflict 裁决的败者归档不经过 archive
17
+ // action(走 applyDecisions 的 conflict 分支),不受此护栏影响。
18
+ const ARCHIVE_GUARDED_TYPES = new Set(["preference", "pattern", "rejected_solution", "constraint", "pitfall"]);
19
+ // 「重复 / 过时」类理由的关键词(中英双语)。命中才允许归档长保留类型;
20
+ // 词表宁可放宽也不误拦——护栏只跳过单条并落明细,不整单拒绝。
21
+ const ARCHIVE_RATIONALE_RE = /重复|过时|陈旧|过期|已被取代|被取代|冗余|不再使用|旧版|废弃|stale|outdated|obsolete|dup(?:licate|licat)?|superseded|redundant|deprecated|expired|replaced/i;
22
+
14
23
  /**
15
24
  * Validate a dream decision list against a snapshot of eligible memories.
16
25
  * @param decisions - LLM-produced decision list. In skipInvalid mode, invalid
@@ -128,6 +137,21 @@ export function validateDecisions(decisions, snapshot, options = {}) {
128
137
  // update-specific field validation runs BEFORE claiming ids, so a failing
129
138
  // update never pollutes the claimed set (which drives the "every id must
130
139
  // appear in a decision" check below).
140
+ // Issue #104 方向 2:archive 类型护栏。长保留类型的 archive 决策必须携带
141
+ // 「重复 / 过时」类理由(提示词对 archive 的定义本就如此),「价值判断」类
142
+ // 理由(保持整洁、参考价值低……)不放行——单条跳过记 skipped(run 记
143
+ // degraded,明细进 dream_runs.skipped),绝不整单拒绝。未知 id 在下方通用
144
+ // 校验里另行报错,这里 snapshot.get 为 undefined 时跳过护栏判断。
145
+ if (local.length === 0 && d?.action === "archive") {
146
+ const guardedTypes = new Set();
147
+ for (const id of d.ids) {
148
+ const mem = snapshot.get(id);
149
+ if (mem && ARCHIVE_GUARDED_TYPES.has(mem.type)) guardedTypes.add(mem.type);
150
+ }
151
+ if (guardedTypes.size > 0 && !ARCHIVE_RATIONALE_RE.test(String(d.reason ?? ""))) {
152
+ local.push(`${at}: archive of long-retention type(s) ${[...guardedTypes].join(", ")} needs a duplicate/outdated rationale (重复/过时), got: ${JSON.stringify(String(d.reason ?? "")).slice(0, 60)}`);
153
+ }
154
+ }
131
155
  if (local.length === 0 && d?.action === "update") {
132
156
  // 只能更新单条
133
157
  if (!Array.isArray(d.ids) || d.ids.length !== 1) {
@@ -216,26 +240,49 @@ export function validateDecisions(decisions, snapshot, options = {}) {
216
240
  if (createCount > maxCreatePerRun) {
217
241
  errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
218
242
  }
243
+ // Issue #104 方向 2:archive 批量上限——一次性大扫除(12 条互不相关主题被
244
+ // 批量归档)是失控信号,与 update 上限同一类全局闸门:skipInvalid 也不豁免,
245
+ // 超限整单拒绝;正常的大规模清理由用户调高 dreamMaxArchivePerRun。
246
+ const archiveCount = survivors.filter((d) => d.action === "archive").length;
247
+ const maxArchivePerRun = options.maxArchivePerRun ?? 8;
248
+ if (archiveCount > maxArchivePerRun) {
249
+ errors.push(`too many archive decisions: ${archiveCount} > ${maxArchivePerRun}`);
250
+ }
251
+ // Issue #89 防洗白核心(#104 方向 1 重审后保留):全部决策都被跳过、合法
252
+ // 子集为空时仍整单拒绝——宽容的前提是「有合法子集可应用」;模型输出整体
253
+ // 非法(如全部幻觉 id)是真故障,不能被隐式 keep 洗成 ok。#104 只豁免
254
+ // 「有合法子集但覆盖率不足」的良性挑重点(见下方 coverageShortfall)。
255
+ if (options.skipInvalid === true && skipped.length > 0 && survivors.length === 0 && errors.length === 0) {
256
+ errors.push(`all ${skipped.length} decision(s) invalid — nothing valid survived validation`);
257
+ return { ok: false, errors, skipped, resolvedShortIds };
258
+ }
219
259
  // v0.4.4: 隐式 keep。默认(dreamImplicitKeep !== false)下,未 claim 的
220
260
  // snapshot 记忆自动补 {action:"keep"},而不是整体拒绝——大记忆量下 LLM 漏报
221
261
  // 一两条就全拒(636 记忆 → 677 errors)会白白浪费整轮 run。设 false 则保留
222
262
  // 旧的严格"全量覆盖"校验。补齐的 keep 直接 append 到 decisions,调用方
223
263
  // (runDream/applyDecisions/audit)复用同一数组即可覆盖全部 snapshot 记忆。
224
264
  //
225
- // v0.4.4 fix(残缺输出防洗白):先收集所有非覆盖类 errors,有错直接 ok:false
226
- // 且绝不 push 任何补齐 keep——残缺决策必须被真实拒绝,不能被隐式 keep 洗白成
227
- // ok 后再 apply。只有无错时才检查显式覆盖率:LLM 输出被截断只 claim 少量
228
- // snapshot(claimed.size / snapshot.size < dreamMinExplicitCoverage)时整单拒绝,
229
- // 而不是用 keep 把绝大部分 snapshot 全部"通过"。
265
+ // v0.4.4 fix(残缺输出防洗白)→ Issue #104 方向 1 重审:显式覆盖率护栏的
266
+ // 初衷是拦「输出截断 → 只 claim 少量 → 其余被隐式 keep 洗白」,但它无法区分
267
+ // 恶性截断与良性挑重点——而提示词硬性规则本身要求「无问题的条目无需输出」,
268
+ // 挑重点正是要求的行为。官方路由 + 强模型的实测(heptaspirit:81 轮 failed
269
+ // 全是 coverage,LLM 调用本身成功、applied=0)证明这是常规路径而非边角。
270
+ // 处置:覆盖率不足不再 ok:false 整单拒绝,改为返回 coverageShortfall 理由、
271
+ // 照常补齐隐式 keep——调用方把 run 记 degraded(合法子集已应用),护栏以
272
+ // 状态暴露而非一票否决,与 skipInvalid 同一宽容哲学。明确异常信号(create
273
+ // 上限等硬错误)保持整单拒绝。要恢复旧行为可设 dreamImplicitKeep:false
274
+ // (严格模式,缺失即 error)或 dreamMinExplicitCoverage: 0(关护栏)。
275
+ // 非覆盖类硬错误(create 上限溢出、严格模式缺失 id)仍然先整单拒绝——
276
+ // #104 方向 1 只豁免 coverage 这一个信号,防洗白语义对它们原样保留。
230
277
  if (errors.length > 0) {
231
278
  return { ok: false, errors, skipped, resolvedShortIds };
232
279
  }
233
280
  const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
281
+ let coverageShortfall = null;
234
282
  if (options.dreamImplicitKeep !== false) {
235
283
  const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
236
284
  if (coverage < minCoverage) {
237
- errors.push(`explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`);
238
- return { ok: false, errors, skipped, resolvedShortIds };
285
+ coverageShortfall = `explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`;
239
286
  }
240
287
  for (const id of snapshot.keys()) {
241
288
  if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
@@ -252,7 +299,7 @@ export function validateDecisions(decisions, snapshot, options = {}) {
252
299
  // 当"被跳过的非法决策数 == 隐式补齐的 keep 数"时长度回到相等但内容已变,
253
300
  // 被跳过的决策会残留进 apply/audit。一律无条件 splice 最安全。
254
301
  decisions.splice(0, decisions.length, ...survivors);
255
- return { ok: true, errors, skipped, resolvedShortIds };
302
+ return { ok: true, errors, skipped, resolvedShortIds, coverageShortfall };
256
303
  }
257
304
 
258
305
  /** Marker thrown when a decision target changed since the run snapshot. */