@modusensus/dsh-mneme 0.10.1 → 0.12.0

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 (97) hide show
  1. package/README.md +17 -6
  2. package/bin/dsh-mneme-choro.mjs +254 -0
  3. package/docs/CONFIGURATION.md +10 -1
  4. package/lib/api-standalone.js +17 -21
  5. package/lib/api.js +414 -12
  6. package/lib/bootstrap-http.js +105 -0
  7. package/lib/bootstrap.js +5 -1
  8. package/lib/choro-command.js +104 -0
  9. package/lib/client.js +1637 -110
  10. package/lib/config-keys.js +144 -0
  11. package/lib/config.js +60 -1
  12. package/lib/document.js +9 -3
  13. package/lib/dream/clustering.js +6 -1
  14. package/lib/index.js +87 -5
  15. package/lib/inject.js +18 -4
  16. package/lib/lang.js +82 -0
  17. package/lib/profile-facts.js +356 -0
  18. package/lib/quality-filter.js +48 -12
  19. package/lib/service.js +112 -4
  20. package/lib/settings.js +13 -1
  21. package/lib/store.js +978 -27
  22. package/lib/tools.js +244 -1
  23. package/lib/user-settings.js +63 -7
  24. package/lib/wiki/inject.js +164 -0
  25. package/lib/wiki/partition.js +200 -0
  26. package/lib/wiki/plan.js +214 -0
  27. package/lib/wiki/queries.js +360 -0
  28. package/lib/wiki/render.js +187 -0
  29. package/lib/wiki/scan.js +371 -0
  30. package/lib/wiki/t0.js +463 -0
  31. package/lib/wiki/t1.js +496 -0
  32. package/lib/write-admission.js +5 -3
  33. package/package.json +4 -2
  34. package/scripts/sync-test-badge.mjs +1 -1
  35. package/scripts/test-count-sync.mjs +9 -5
  36. package/src/api-standalone.js +17 -21
  37. package/src/api.js +414 -12
  38. package/src/bootstrap-http.js +105 -0
  39. package/src/bootstrap.js +5 -1
  40. package/src/choro-command.js +104 -0
  41. package/src/config-keys.js +144 -0
  42. package/src/config.js +60 -1
  43. package/src/document.js +9 -3
  44. package/src/dream/clustering.js +6 -1
  45. package/src/index.js +87 -5
  46. package/src/inject.js +18 -4
  47. package/src/lang.js +82 -0
  48. package/src/profile-facts.js +356 -0
  49. package/src/quality-filter.js +48 -12
  50. package/src/service.js +112 -4
  51. package/src/settings.js +13 -1
  52. package/src/store.js +978 -27
  53. package/src/tools.js +244 -1
  54. package/src/user-settings.js +63 -7
  55. package/src/wiki/inject.js +164 -0
  56. package/src/wiki/partition.js +200 -0
  57. package/src/wiki/plan.js +214 -0
  58. package/src/wiki/queries.js +360 -0
  59. package/src/wiki/render.js +187 -0
  60. package/src/wiki/scan.js +371 -0
  61. package/src/wiki/t0.js +463 -0
  62. package/src/wiki/t1.js +496 -0
  63. package/src/write-admission.js +5 -3
  64. package/test/api-routes-gaps.test.js +131 -1
  65. package/test/api.test.js +9 -1
  66. package/test/bootstrap.test.js +8 -0
  67. package/test/choro-command.test.js +218 -0
  68. package/test/client.test.js +802 -21
  69. package/test/config-keys-routes.test.js +233 -0
  70. package/test/config.test.js +79 -0
  71. package/test/contrast.test.js +14 -7
  72. package/test/document.test.js +32 -0
  73. package/test/embedding-cache-f32.test.js +276 -0
  74. package/test/inject-pin-explicit.test.js +300 -0
  75. package/test/inject-pin-vector-crowd.test.js +225 -0
  76. package/test/inject.test.js +51 -0
  77. package/test/llm-audit.test.js +29 -1
  78. package/test/persona-settings.test.js +31 -0
  79. package/test/profile-facts.test.js +492 -0
  80. package/test/quality-backfill.test.js +290 -0
  81. package/test/quality-filter.test.js +113 -4
  82. package/test/service.test.js +1 -1
  83. package/test/session-gate.test.js +1 -1
  84. package/test/test-count-sync.test.js +14 -3
  85. package/test/tool-state-ignore.test.js +65 -0
  86. package/test/tools-dto-schema-matrix.test.js +23 -2
  87. package/test/tools.test.js +144 -7
  88. package/test/wiki-api.test.js +273 -0
  89. package/test/wiki-inject.test.js +265 -0
  90. package/test/wiki-partition.test.js +112 -0
  91. package/test/wiki-plan.test.js +80 -0
  92. package/test/wiki-queries.test.js +270 -0
  93. package/test/wiki-scan.test.js +208 -0
  94. package/test/wiki-store.test.js +190 -0
  95. package/test/wiki-t0.test.js +380 -0
  96. package/test/wiki-t1.test.js +235 -0
  97. package/test/wiki-tools.test.js +176 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
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-2681%20passed-success)](https://github.com/slow-stack/mneme)
8
+ [![tests](https://img.shields.io/badge/tests-2881%20total-success)](https://github.com/slow-stack/mneme)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
10
10
  [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
11
11
  [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
@@ -49,7 +49,7 @@ dsh web
49
49
  - 逐 type 记录 `committed / failed / pending` 回执,健康端点区分 `ok / degraded / unknown`
50
50
  - 状态写失败不静默:同步失败落日志并留债务,重启自动收敛
51
51
 
52
- ### 模型工具(默认注册 11 个;开 `organizeEnabled` 后 12 个)
52
+ ### 模型工具(默认注册 12 个;开 `organizeEnabled` 后 13 个)
53
53
 
54
54
 
55
55
  | 工具 | 功能 |
@@ -62,11 +62,14 @@ dsh web
62
62
  | `memory_delete` | 删除记忆(按记忆 ID 精确删除) |
63
63
  | `memory_forget` | 抑制注入(降权不删除,可恢复) |
64
64
  | `memory_archive` | 归档/恢复记忆(v0.2.5;归档后隐藏于列表/搜索/注入/整理,`archived=false` 可恢复) |
65
+ | `memory_pin` | 钉住一条记忆:无论类型、无论它和其他记忆怎么竞争,每轮都注入在记忆块最前面(#249 第二批;受 `pinnedInjectBudget` 上限约束,钉的顺序是「先钉的占位」,预算不够时最早钉的优先)。**只在她明确说「这条必须一直在」时用**,不是标记重要性的手段——重要性本来就参与排序。钉着 ≠ 会注入:遗忘 / 归档 / 暂停作用域都优先于 pin,回执里的 `injectable` 如实报告这件事。`unpin: true` 松开 |
65
66
  | `set_scope_state` | 按下/松开某个作用域的**可逆闸门**(#471:政策轴。暂停期间该作用域的记忆照旧存着,只是不注入、不参与巩固,松开即原样回来;不改任何一条记忆的标注,与上面逐条操作的 `memory_forget` / `memory_archive` 是两根不同的轴)。两级开关 `scopeState.enabled` / `.enforce` 默认都关,没开时按下不产生拦截效果 |
66
67
  | `memory_register_document` | 登记 agent 产长文档的指针行(#230:摘要 + `doc_path` + evidence 落 `document` 类型;正文留在磁盘、由 agent 自管,注册口唯一,旁路写入被拒) |
67
68
  | `memory_organize` | 主动整理(#231,**要 `organizeEnabled` 打开才注册**):`mode=dryRun` 先出比对报告(每条候选 `exact` / `near` / `new` + 命中行,**不写一行库**、只留审计回执),agent 判断后 `mode=apply` 引用那份报告的 `run_id` 落地。两种「不要它」要分清——**`discard`** = 这条候选不写,什么都不动(只进回执);**`archive`** = 把报告标为被取代的那条**既有行**归档(可恢复,绝不物理删除)。没有比对过的候选写不进库,一份报告也只许落地一次 |
68
69
  | `memory_runtime` | 本地推理运行时的状态 / 供给 / 校验(v0.7.32,自管运行时三档取件) |
69
70
 
71
+ 开启 `chorographyEnabled` 后可用仓库地志工具。多个仓库已登记时请传仓库 ID。若搜索只读到部分页面,结果会标明漏查的页。
72
+
70
73
  ### 工作流(M.O.,v0.9 beta)
71
74
 
72
75
  记忆库答的是「这件事**为什么**这么办」,答不了「这件事**到底怎么跑**」。v0.9 把后者从记忆库里的一行文字变成项目里可提交、可 review、可被 CI 读的一份文件——一条工作流一个 JSON:
@@ -247,6 +250,12 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
247
250
 
248
251
  > 只读端点,与 list/search/semantic 一样在设置 `apiToken` 后仍保持开放。
249
252
 
253
+ 沉淀曲线(面板状态页「8 周沉淀」图的数据源):
254
+
255
+ - `GET /api/dsh-mneme/semantic/consolidation-stats?days=` — 近 N 天(缺省 56,钳 1–365)按天返回三条序列:`added`(新建记忆,排除外来导入行)、`merged`(当天被巩固掉的**去重记录数**,取自 `receipt_chain` 里 `kind∈(merge,update)` 且 `verdict='live'`)、`archived`(当天进入归档面的条数),另附三个窗口合计。
256
+
257
+ `merged` 刻意按去重 `record_id` 数而不是行数:同一条记录被多轮巩固重复记账是常态(活库实测 2412 行只对应 1796 个记录),按行数会把「又记了一遍」算成「又沉淀了一条」。新增与合并背离才是这张图要看的东西——2026-10-25–10-31 那段新增照旧、合并归零,只看新增或只看总量都发现不了。
258
+
250
259
  ### 记忆库主区视图与记忆图谱 🕸️(v0.5.0)
251
260
 
252
261
  记忆功能从侧边栏抽屉收进**主内容区全宽 tab**(conversation.view 插槽,与「对话 / Trajectory」并列),侧边栏「记忆」入口点击后直接激活该 tab。页面顶部子 tab 行切换三个子视图:
@@ -286,6 +295,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
286
295
  | **v0.9.5**(2026-10-30) | 记忆能安全地递给别人,也能整包收回来:**分享包 `mneme.share/v1`**(`dsh-mneme share` / `receive` / `undo-share`,面板「⋯」里同一批三件事)只含活跃行,scope 与 home 前缀不进包,命中密钥 / PII 的行整条不进;出包先给报告再给文件,超 3 MiB 水位回 413、按 `--type` / `--tag` 分批;入包一条不合格整包拒、干跑为默认、按 `share_external_id` 幂等,整包退出走物删。「本机巩固不许改写别人导入的原文」由单一谓词 `isImportedRow` 守在汇流口与九个行池上。同版给记忆来源加了读出口 `GET /memory/provenance`(#421,@z2Ace0107 实现;409 = 用户关了、503 = 装配坏了),修掉同标题重复入库(#505)、扫描谎报「没东西可扫」(#515)、五处未围栏的巩固 prompt(#501)与 dream 合并护栏整类误拦(#521),面板新增设置页分组目录(#518) |
287
296
  | **v0.10.0**(2026-11-02) | 归因与闸门——给已经发生的事补上它自己的身份:**蒸馏有了运行身份**(窗口级 `distill_runs` 记 `ok`/`degraded`/`noop`/`failed`/`aborted` 与 `audit_id`,行级 `memory_mints` 记「这条记忆出生自哪一轮巩固」;历史行留空**不猜**,失败轮也真记一行),此前归因只到「会话区间」——那是输入的坐标不是运行的身份;**溯源补上反方向** `GET /memory/provenance?sessionId=`(两个主键互斥,`limit` 越界 400 不静默截断,撞上限带 `truncated: true`);**会话级无痕闸门** `sessionGateEnabled`(指定会话与子代理不进不出,其余会话照常;子代理判据只认宿主委派标记,`parentSession` 不是判据——本机 641 个会话头实测会误判 3 个 fork 主会话);**防谄媚补到写入面**(蒸馏与总览加「立场纪律」四条与「叙述纪律」,排在与时间换算的指令之前)。另修 `differentiate` 正反馈闭环(注记改落 `differentiated_pairs` 账本、正文一个字节都改不了,实测单条曾堆 19 段)、sleep 冲突相位的输入侧预算与续排(分批后不再隔夜才轮到)、面板「最近巩固」区分 organize 回执(#495)、面板首次有移动端与 481–519px 中间带断点(#542) |
288
297
  | **v0.10.1**(2026-11-03) | 打开记忆详情不再崩掉整个宿主:0.10.0 里点开侧边栏任意一条记忆的详情抽屉,渲染进程立刻抛 `ReferenceError: provenance is not defined`,整个宿主跟着重启、进行中的会话被打断,100% 复现(#553)。根因是两个隔了三千多行的组件——`MemoryDrawer` 的归因 effect 与渲染块在读 `provenance` / `setProvenance`,而那句 `useState` 落在了同文件**上一个**组件 `EntityPanel` 里,在那里是纯死代码(整个函数体除声明外对 `provenance` 零引用)。这个形状躲得过眼睛、语法检查与原有用例,所以补的不是「锁一个名字」的用例,而是按组件扫「调了 `setXxx(` 却没有声明」。同版带上 v0.10.0 之后合入的图谱锚定层收尾(#24):Tag 与向量两路起点接线(`normalizePath` 补回通路身份、`findEntitiesByTags` 走全等匹配并按 500 分块、反查改 UNION 三源以覆盖只看关系不建 attr 行的记忆、向量路复用同一份 query 向量并滤掉 0 分种子),以及被动确认的复核出口(`GET /semantic/graph/boosted-relations` 按「合法抬升量 > 0」列出、ego 边补 `weight`/`source`、面板默认收起的审计视角) |
298
+ | **v0.11.0**(2026-11-05) | 仓库地志(chorography)落地并接到消费面:读一个 JS/TS 仓库,把文件树与 import 图编译成模块划分与 `.chorography/` 下的 Markdown 地志页——T0 骨架全程零模型零网络、`plan.yaml` 正面控制、`CHORO:HUMAN` 保留区跨重生成逐字节存活;T1 逐模块一次 headless 会话补架构/约定/技术栈/构建四段语义正文并落可核验的 claims(三重上限、编造锚点整轮拒)。消费面三张脸:面板第二座位 `dsh-mneme-wiki`、三个只读工具、`/choro/*` 读路由,外加每轮开局一小块标题级提纲注入(`chorographyInject` = light/full/off)与只读 `/choro` 命令(过期只提示、绝不自动重跑)。`chorographyEnabled` opt-in 默认关。wiki 行与记忆行物理隔离,永不进巩固/质量过滤任何行池。另修 #573:宽态侧栏的记忆入口落在「更多」行下面、被展开的菜单盖住 |
299
+ | **v0.12.0**(2026-11-07) | pin 从「按类型推断」变成你亲手钉的显式事实:另开 `pinned_memories` 表 + `memory_pin`(取消钉选是同一个工具传 `unpin: true`,没有单独的 unpin 工具),钉住的行在注入里优先占位,不再依赖它恰好是 `constraint` / `preference`;顺带修掉两处旁路——pin 筛选发生在 hybrid 合并之后、而合并结果已按 `poolSize` 封顶(生产 10 条),规则窗口里的 pin 行一条都进不了候选池(真库副本 pin 注入 2/5 → 5/5、总数 7 → 10,原有条目只后移无一条被挤出),判据也从「表里有几行」改成「有几条能用」(pin 行会在记忆侧被归档 / 遗忘 / 硬删而 pin 表一无所知,旧判据只要剩一行就永不回落;`pinnedStats.source` 分 `explicit` / `fallback-none-usable` / `type-fallback` 三档)。`pinnedInjectBudget` **默认 0,即默认状态下 pin 池整体关闭**——钉住不等于会抢占注入位次,要显式设成正数才启用,关着时注入序列与改前逐条相同。同版带上「Agent 眼中的你」自动事实(`profileFactsEnabled` 默认关,按身份分卡维护、参与注入、面板可管)、向量解析缓存从恒 0% 命中到跟工作集自适应(真库四轮 0.0% → 第二轮起 100%,A/B 1.8–2.4s → 370–424ms,存储换 `Float32Array` 55.5MB → 29.7MB,并摘掉「只取 id 却带 `embedding != ''`」的谓词)、[#579](https://github.com/slow-stack/mneme/issues/579) 地志工具改走宿主错误路径且检索不再把「读不到」答成「没匹配」(@Oscar-Williams)、[#590](https://github.com/slow-stack/mneme/issues/590) 严格寻址不再把「一张卡都没建」判成「没有这张卡」,以及面板跨组件广播 `mneme:store-changed`、chorography 收尾(面板「重建骨架」+ 页级向量检索)。 |
289
300
 
290
301
  | **v0.8.x** | 作用域隔离(agent / workspace 双维 + opt-in 硬过滤)、冲突队列人工裁决、生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断) |
291
302
 
@@ -685,7 +696,7 @@ Claude Code 挂载示例(项目根 `.mcp.json`;token 在面板「设置 →
685
696
  │ 服务层:saveWithDedupe / injectCandidates │
686
697
  │ / mergeHumanEdits / onWrite 钩子 │
687
698
  ├─────────────────────────────────────────────────┤
688
- │ 模型接口:11 个工具 + 自动注入 + 会话摘要 │
699
+ │ 模型接口:12 个工具 + 自动注入 + 会话摘要 │
689
700
  ├─────────────────────────────────────────────────┤
690
701
  │ autoDream:阈值调度 → LLM 决策清单 │
691
702
  │ → 校验(fail-safe)→ 应用 → 摘要 │
@@ -703,7 +714,7 @@ src/
703
714
  ├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子、scope 加权/硬过滤、冲突队列)
704
715
  ├── scope.js # 作用域解析(agentPreset + workspace registry 反查,issue #17)
705
716
  ├── config.js # schemastery 配置 schema
706
- ├── tools.js # 11 个模型工具(defineTool;memory_organize 要 organizeEnabled 打开才注册,开了是 12 个)
717
+ ├── tools.js # 12 个模型工具(defineTool;memory_organize 要 organizeEnabled 打开才注册,开了是 13 个)
707
718
  ├── user-settings.js # 「用户设置」注入块(人格 → 画像 → 规则)的唯一渲染处,注入与面板预览共用
708
719
  ├── profile-extract.js # 从活跃记忆提取「助手眼中的用户」画像草稿(只出草稿,不写 user_settings)
709
720
  ├── organize.js # agent 主动整理:dryRun 比对报告 → agent 判断 → apply 落库(筛除=归档不删,留 dream_runs 回执)
@@ -722,7 +733,7 @@ src/
722
733
  ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
723
734
  └── index.js # 插件接线
724
735
  lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
725
- test/ # 2681 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
736
+ test/ # 2881 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
726
737
  scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
727
738
  ```
728
739
 
@@ -731,7 +742,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
731
742
  ```bash
732
743
  cd dsh-mneme
733
744
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
734
- npm test # 运行 2681 个测试
745
+ npm test # 运行 2881 个测试
735
746
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
736
747
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
737
748
  ```
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ // bin/dsh-mneme-choro.mjs —— chorography(仓库地志)T0 确定性骨架的命令行入口。
3
+ //
4
+ // 为什么独立成 bin 而不是 cli.mjs 的子命令:CONTRIBUTING「The CLI is dependency-free
5
+ // by contract」禁止给 bin/cli.mjs 加 import,而 T0 要挂 lib/ 的扫描/划分/渲染与
6
+ // store(写 choro_* 表);命名循 dsh-mneme-serve / dsh-mneme-mcp 先例。
7
+ //
8
+ // 为什么缺省 dry-run、--apply 才写:会动仓库磁盘的东西先给数字再执行(reclaim 同款
9
+ // 纪律)。dry-run 零写入,receipt 里有「会写哪些文件、各多大」。
10
+ //
11
+ // T1(--t1)沿用同一条纪律,且以「花不花钱」再加一道:dry-run 只列候选模块与跳过
12
+ // 原因,--apply 才真开 headless 会话(吃宿主模型额度)。
13
+ //
14
+ // stdout 纪律:一次成功/dry-run 的 receipt 走 stdout(人读或 --json 机器读),错误
15
+ // 与拒绝走 stderr;exit 0 = dry-run 或成功,exit 1 = 拒绝/失败。
16
+ import fs, { readFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join, resolve, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { createStore } from "../lib/store.js";
21
+ import { runChoroT0, ChoroRefusal } from "../lib/wiki/t0.js";
22
+ import { createT1, DEFAULT_T1_MODEL, T1Refusal } from "../lib/wiki/t1.js";
23
+
24
+ const BIN_NAME = "dsh-mneme-choro";
25
+ const PKG = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
26
+
27
+ const USAGE = `${BIN_NAME} — 仓库地志(chorography):T0 确定性骨架 + T1 语义生成轮
28
+
29
+ 用法:
30
+ ${BIN_NAME} <repo-path> [选项]
31
+
32
+ 选项:
33
+ --apply 实际写入 <repo>/.chorography/ 并落库(缺省 dry-run,只报数字)
34
+ --t1 T1 语义生成轮:逐模块一轮 headless 会话补四段语义正文 + claims。
35
+ 缺省 dry-run 只列候选;--apply 才开会话(花宿主模型额度)
36
+ --module <slug> 只跑一个模块(T1)
37
+ --model <p/m> T1 模型路由(缺省 xdpool/deepseek-v4.1-flash)
38
+ --timeout <秒> T1 单模块墙钟上限(缺省 240s)
39
+ --retry-failed 把 failed 页拉进来重试(T1;成功后计数清零)
40
+ --dsh-exe <路径> DeepSeek Harness 主程序定位(缺省走 DSH_EXE / 标准安装位)
41
+ --db <path> memory.db 路径(缺省 ~/.dsh/memory/memory.db,前导 ~ 会展开)
42
+ --json receipt 用机器可读 JSON 输出
43
+ -h, --help 本帮助
44
+ -V, --version 版本
45
+
46
+ T0 是确定性骨架(零模型、可离线):文件树 + import 图 → 模块划分 → 骨架页。
47
+ T1 用 headless 会话补语义段落与溯源 claims(逐模块、三重上限、失败不推进)。
48
+
49
+ 页面里 CHORO:HUMAN 标记块的内容在重生成时逐字节保留;块外手改不保留。
50
+ `;
51
+
52
+ function parseArgv(argv) {
53
+ const positionals = [];
54
+ const flags = {};
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const arg = argv[i];
57
+ if (arg === "--apply") flags.apply = true;
58
+ else if (arg === "--json") flags.json = true;
59
+ else if (arg === "--t1") flags.t1 = true;
60
+ else if (arg === "--retry-failed") flags.retryFailed = true;
61
+ else if (arg === "--module" || arg === "--model" || arg === "--dsh-exe") {
62
+ const next = argv[i + 1];
63
+ if (next === undefined || next.startsWith("--")) {
64
+ throw new Error(`${arg} 需要一个参数`);
65
+ }
66
+ flags[arg.slice(2)] = next;
67
+ i++;
68
+ } else if (arg === "--timeout") {
69
+ const next = argv[i + 1];
70
+ if (next === undefined || next.startsWith("--")) {
71
+ throw new Error("--timeout 需要一个秒数");
72
+ }
73
+ // 现算一次:NaN 会一路混进 spawn 的 timeoutMs,表现成「上限不生效」而不是报错。
74
+ const secs = Number(next);
75
+ if (!Number.isFinite(secs) || secs <= 0) throw new Error(`--timeout 需要正数秒:${next}`);
76
+ flags.timeout = secs;
77
+ i++;
78
+ } else if (arg === "--db") {
79
+ const next = argv[i + 1];
80
+ if (next === undefined || next.startsWith("--")) {
81
+ throw new Error("--db 需要一个路径参数");
82
+ }
83
+ flags.db = next;
84
+ i++;
85
+ } else if (arg === "-h" || arg === "--help") flags.help = true;
86
+ else if (arg === "-V" || arg === "--version") flags.version = true;
87
+ else if (arg.startsWith("--")) throw new Error(`未知选项: ${arg}`);
88
+ else positionals.push(arg);
89
+ }
90
+ return { positionals, flags };
91
+ }
92
+
93
+ // 前导 ~ 展开,语义与 src/serve.js 同款(~ 只在开头、后面跟 / 或 \ 或结尾)。
94
+ function expandHome(path) {
95
+ const home = homedir();
96
+ return String(path).replace(/^~(?=$|[\\/])/, home);
97
+ }
98
+
99
+ function defaultDbPath() {
100
+ return expandHome("~/.dsh/memory/memory.db");
101
+ }
102
+
103
+ async function main() {
104
+ const argv = process.argv.slice(2);
105
+ let parsed;
106
+ try {
107
+ parsed = parseArgv(argv);
108
+ } catch (err) {
109
+ console.error(String(err?.message ?? err));
110
+ process.exit(1);
111
+ }
112
+ const { positionals, flags } = parsed;
113
+ if (flags.version) {
114
+ console.log(PKG.version);
115
+ return;
116
+ }
117
+ if (flags.help) {
118
+ console.log(USAGE.trimEnd());
119
+ return;
120
+ }
121
+ if (positionals.length === 0) {
122
+ console.error(USAGE.trimEnd());
123
+ console.error(`\n缺仓库路径:${BIN_NAME} <repo-path> [--apply]`);
124
+ process.exit(1);
125
+ }
126
+ if (positionals.length > 1) {
127
+ console.error(`多余的位置参数:${positionals.slice(1).join(" ")}(仓库路径只有一个,含空格请加引号)`);
128
+ process.exit(1);
129
+ }
130
+
131
+ const repoPath = resolve(positionals[0]);
132
+ const dbPath = resolve(expandHome(flags.db ?? defaultDbPath()));
133
+
134
+ let store;
135
+ try {
136
+ store = createStore(dbPath);
137
+ } catch (err) {
138
+ console.error(`打开 memory.db 失败(${dbPath}):${String(err?.message ?? err)}`);
139
+ process.exit(1);
140
+ }
141
+
142
+ try {
143
+ if (flags.t1) {
144
+ // T1 档(M2):缺省 dry-run 只列候选模块与章程预览(零会话零写入);
145
+ // --apply 才真开会话花钱。三重上限与降级表在 createT1 里。
146
+ const t1 = createT1({ store });
147
+ const receipt = await t1.runT1({
148
+ repoRoot: repoPath,
149
+ model: flags.model ?? DEFAULT_T1_MODEL,
150
+ moduleSlug: flags.module ?? null,
151
+ retryFailed: flags.retryFailed === true,
152
+ dryRun: flags.apply !== true,
153
+ dshExe: flags["dsh-exe"] ?? null,
154
+ limits: flags.timeout ? { timeoutMs: flags.timeout * 1000 } : null,
155
+ });
156
+ console.log(flags.json ? JSON.stringify(receipt, null, 2) : renderT1Receipt(receipt, { apply: flags.apply === true }));
157
+ return;
158
+ }
159
+ const receipt = runChoroT0({ repoRoot: repoPath, store, apply: flags.apply === true });
160
+ console.log(flags.json ? JSON.stringify(receipt, null, 2) : renderReceipt(receipt));
161
+ } catch (err) {
162
+ if (err instanceof ChoroRefusal || err instanceof T1Refusal) {
163
+ console.error(`拒绝运行(${err.code}):${err.message}`);
164
+ } else {
165
+ console.error(`${BIN_NAME} 失败:${err?.stack ?? String(err)}`);
166
+ }
167
+ process.exitCode = 1;
168
+ } finally {
169
+ try { store.close(); } catch { /* WAL 关闭异常不遮蔽主结果 */ }
170
+ }
171
+ if (process.exitCode) process.exit(process.exitCode);
172
+ }
173
+
174
+ function renderT1Receipt(r, { apply }) {
175
+ const lines = [];
176
+ const head = r.dryRun ? "T1 dry-run(未开任何会话;确认后加 --apply)" : "T1 已跑";
177
+ lines.push(`${head} · ${r.repoName} · 模型 ${r.model}`);
178
+ if (r.t1Unavailable) {
179
+ lines.push(`T1 整档不可用(环境级,页状态未动):${r.t1Unavailable}`);
180
+ lines.push("候选模块仍在下面,环境修好后重跑即可。");
181
+ lines.push("");
182
+ }
183
+ if (r.dryRun) {
184
+ lines.push(`将富化的模块(${r.candidates.length}):`);
185
+ for (const c of r.candidates) lines.push(` ${c.slug.padEnd(24)} ${String(c.fileCount).padStart(4)} 文件 · ${c.status}`);
186
+ lines.push(`跳过(${r.skipped.length}):`);
187
+ for (const s of r.skipped) lines.push(` ${s.slug} — ${s.reason}`);
188
+ lines.push(`三重上限:${r.limits.timeoutMs}ms 墙钟 / ${r.limits.maxToolCalls} 次工具 / ${r.limits.maxOutputBytes}B 输出`);
189
+ lines.push("");
190
+ lines.push("每个模块一轮 headless 会话,会花宿主模型额度;确认后加 --apply。");
191
+ } else {
192
+ lines.push(`富化 ${r.enriched.length} · 失败 ${r.failed.length} · 跳过 ${r.skipped.length}${r.t1Unavailable ? " · 整档中止(见上)" : ""}`);
193
+ for (const e of r.enriched) lines.push(` ✓ ${e.slug}(${e.sections.join("/")} · claims ×${e.claims})`);
194
+ for (const f of r.failed) lines.push(` ✗ ${f.slug} — ${f.reason}${f.marked ? "(连续失败达阈值,标记 failed)" : ""}`);
195
+ for (const s of r.skipped) lines.push(` · ${s.slug} — ${s.reason}`);
196
+ }
197
+ for (const w of r.warnings ?? []) lines.push(`提示:${w}`);
198
+ return lines.join("\n");
199
+ }
200
+
201
+ function renderReceipt(r) {
202
+ const lines = [];
203
+ const head = r.dryRun ? "dry-run(未写任何东西;确认后加 --apply)" : "已写入";
204
+ lines.push(`${head} · ${r.repoName} @ ${r.repoPath}`);
205
+ lines.push(`git 基线 ${r.gitBaseline} · 计划 ${r.planPath ?? "(无 plan.yaml,走默认启发式)"}`);
206
+ lines.push(
207
+ `扫描 ${r.fileCount} 个文件 → ${r.moduleCount} 个模块 → ${r.pageCount} 页` +
208
+ (r.skippedByWhitelist > 0 ? `(白名单挡掉 ${r.skippedByWhitelist} 页)` : "") +
209
+ ` · generatedAt ${r.generatedAt}${r.reusedGeneratedAt ? "(沿用上次)" : ""}`
210
+ );
211
+ if (r.skippedFiles.length > 0) {
212
+ lines.push(`跳过 ${r.skippedFiles.length} 个读不了的文件:`);
213
+ for (const s of r.skippedFiles.slice(0, 5)) lines.push(` · ${s.file} — ${s.reason}`);
214
+ if (r.skippedFiles.length > 5) lines.push(` … 共 ${r.skippedFiles.length} 个`);
215
+ }
216
+ if (r.unresolvedCount > 0) lines.push(`${r.unresolvedCount} 条相对引用未解析(别名/动态路径),未计入依赖图。`);
217
+ lines.push("");
218
+ lines.push("模块划分:");
219
+ for (const m of r.moduleSummary) {
220
+ lines.push(` ${m.slug.padEnd(28)} ${String(m.fileCount).padStart(4)} 文件 · ${m.internalEdges} 边 · ${m.externalDeps} 外部依赖 · ${m.title}`);
221
+ }
222
+ lines.push("");
223
+ if (r.dryRun) {
224
+ lines.push("将写入:");
225
+ for (const w of r.wouldWrite) lines.push(` ${w.path}(${w.bytes} B)`);
226
+ lines.push(`会保留的 CHORO:HUMAN 保留区:${r.humanBlocksSeen} 块`);
227
+ lines.push("");
228
+ lines.push("确认无误后加 --apply 执行。");
229
+ } else {
230
+ lines.push(`写入 ${r.bytes} B;保留区 ${r.humanBlocksPreserved} 块逐字节未动;choro_* 表已整仓替换。`);
231
+ }
232
+ for (const w of r.warnings) lines.push(`提示:${w}`);
233
+ return lines.join("\n");
234
+ }
235
+
236
+ // 直接运行才进 main(import 只取 parseArgv 做单测时必须安静):npm 全局安装走
237
+ // node_modules/.bin 的符号链接,argv[1] 是链接路径而非真身,所以 realpath 两侧再比。
238
+ const invokedDirectly = (() => {
239
+ if (process.argv[1] == null) return false;
240
+ try {
241
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
242
+ } catch {
243
+ return false;
244
+ }
245
+ })();
246
+
247
+ if (invokedDirectly) {
248
+ main().catch((err) => {
249
+ console.error(err?.stack ?? String(err));
250
+ process.exit(1);
251
+ });
252
+ }
253
+
254
+ export { parseArgv };
@@ -12,6 +12,11 @@
12
12
  - 行为开关(布尔键)默认关的即 opt-in:不开 = 行为与该键存在之前逐字节一致。
13
13
  - 绝大多数行为开关与部分阈值/路由键在 `settings.js` 的白名单里,可在面板「功能开关」
14
14
  逐项启停(线上回滚开关);未入白名单的键(路径、词典、枚举外的杂项)只能改 bundle 配置。
15
+ - **「面板能改 / 只能改文件」不用在这里逐行标注**:面板「设置 → 连接 → 配置键覆盖面」
16
+ 那张只读卡会实时把两者交/差出来(`GET /api/dsh-mneme/config-keys`,只回键名与计数、
17
+ 不回任何值)。清单由 `src/config-keys.js` 从 schema 与白名单现算,改哪边都不会漏。
18
+ 那里还会单独列出「白名单登记了、但 schema 没给默认值」的键——这类键在面板上有输入框,
19
+ 留空时却不会进「当前生效值」。
15
20
 
16
21
  ## 全局与存储
17
22
 
@@ -162,7 +167,8 @@
162
167
  | `injectTimePrefix` | `false` | 对话开始注入一次当前时间(#34,v0.7.11 误删后随 #333 恢复) | opt-in;格式与键名与 v0.7.2 一致,存量 feature_flags 恢复即生效 |
163
168
  | `personaEnabled` | `false` | 人格段:用户自写的一小段常驻注入文本 | opt-in;只是轻量常驻文本、不引入重型后台路径,故不进 `LIGHT_MODE_OFF` |
164
169
  | `personaStrictAddressing` | `false` | 人格寻址口径。关(历史行为):会话身份没建卡、或该卡被显式关掉时,落 `default` 卡 | 开:这两种情况都不注入人格,`default` 只兜「解析不到会话身份」的无名会话。用途是宿主里跑着自带身份的外部 bot 时,别让某个人格无条件进它每一帧。默认关 = 升级零变化 |
165
- | `profileExtractEnabled` **light** | `false` | 画像提取草稿:`POST /api/dsh-mneme/profile/extract` 从活跃记忆反向写一段「助手眼中的用户」草稿(面板「用户画像」卡上的按钮) | opt-in;一次调用一轮 LLM + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它。**只出草稿**:它不写 `user_settings`,采用草稿是面板里另一次显式保存——画像正文必须是用户手写的原文。空库直接报 `no-input`、模型没按格式回报 `unparseable`,两者都不回空草稿 |
170
+ | `profileExtractEnabled` **light** | `false` | 画像提取草稿:`POST /api/dsh-mneme/profile/extract` 从活跃记忆反向写一段「助手眼中的用户」草稿(面板「Agent 眼中的你」卡上的按钮) | opt-in;一次调用一轮 LLM + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它。**只出草稿副本**:它不写 `user_settings`,面板也只把结果当副本展示、可一键复制(2026-11-04 改口径,此前「采用为画像」会直接覆盖手写画像且无撤销)——画像正文必须是用户自己手写的原文。空库直接报 `no-input`、模型没按格式回报 `unparseable`,两者都不回空草稿 |
171
+ | `profileFactsEnabled` **light** | `false` | 自动画像事实:「Agent 眼中的你」按会话身份分卡维护一份逐条事实清单(对标 ChatGPT 记忆 / DS Bot 每 bot 的 MEMORY.md)——dream 巩固轮成功后逐卡增量维护(有新记忆才跑),注入在手填画像之后;面板可手动触发与逐条增改归档,账本逐 op 可查 | opt-in;自动维护轮是 dream 级 LLM 调用 + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它(注入段本身是几十行常驻文本,关掉省的是维护调用不是注入)。事实住 `profile_facts` 独立三表,**绝不写 `user_settings`**(#497 口径不变:手填画像是用户原文,模型只有读权)。输入行严格按 `agent_scope` 分区、排除外来行;账本 `profile_fact_journal` 每卡裁剪到最近 200 条 |
166
172
 
167
173
  ## 巩固(autoDream)
168
174
 
@@ -310,6 +316,8 @@
310
316
  | `disableMemorySearch` | `false` | 对模型隐藏 `memory_search` 工具(v0.8.5) | 慢/轻量模型的工具往返节流;隐藏即不可调 |
311
317
  | `disableMemoryArchive` | `false` | 对模型隐藏 `memory_archive` 工具 | 同上 |
312
318
  | `organizeEnabled` | `false` | 对模型开放 `memory_organize` 工具(#231:先 dryRun 出比对报告,agent 判断后 apply 落库) | 与上面两个**反向**:那两个藏默认在场的工具,本键开默认不在场的能力。关 = 工具根本不注册(模型看不到就不会调)。dryRun→apply 是两步协议,弱模型容易漏 `run_id` 或跳过报告,所以 `LIGHT_MODE_OFF` 里有它;apply 只能引用一次真实报告、且一份报告只许落地一次 |
319
+ | `chorographyEnabled` | `false` | chorography(仓库地志,spec 见 docs/specs/)暴露闸:对模型开放三个只读工具(`choro_search` / `choro_read_page` / `choro_module_map`)+ 面板「仓库地志」浏览页 + 每轮的开局提纲注入 + `/choro` 命令 | 与 `organizeEnabled` 同款反向语义:关 = 工具根本不注册、面板页空态、注入块不渲染、命令不注册,行为与旧版逐字节一致。本键是「是否启用这个子系统」,不是「每轮付多少」——所以它仍**不进 `LIGHT_MODE_OFF`**,轻量档要压的是下面那条 `chorographyInject`。检索是关键词子串(M1);生成走 `dsh-mneme-choro` CLI(T0 确定性骨架 + `--t1` headless 语义富化) |
320
+ | `chorographyInject` | `"light"` | 开局注入档位:`light` 只列文件数最多的 6 个模块(约 8 行)、`full` 列全部模块、`off` 不注入 | 只注模块名 / 文件数 / 页状态 / 页相对路径,**不含正文**(正文由模型 `choro_read_page` 取);注入块折进既有 memory context,不新开 context(spec §7「合并成一次拼装」)。`lightMode: true` 时在消费侧压成 `off`(见 `wiki/inject.js` 的 `resolveInjectTier`)——它**不进 `LIGHT_MODE_OFF` 表**,因为那张表对平铺键一律赋 `false`,枚举值塞进去会与 schema 不符 |
313
321
  | `actionPipelineEnabled` | `false` | M.O. 只读视图的门控:做法图谱 + 对话行结算卡 | opt-in beta;只是纯 UI 入口、不引入重型后台路径,故不进 `LIGHT_MODE_OFF`。写账另有独立的执行记账键(`/mo-run`),看图 ≠ 写账 |
314
322
  | `actionRunEnabled` | `false` | M.O. 执行记账:`/mo-run` 开一次执行窗口,窗口内命令执行才可能被记成一条 run | opt-in beta;挂在热路径(每个会话事件都过一次监听),故 `LIGHT_MODE_OFF` 里有它 |
315
323
  | `actionNudgeEnabled` | `false` | M.O. 自动沉淀:每轮结束(`turn/end`)后挖一次「这个项目重复走的动作」,结果落成 `memoryDir/proposals/<项目键>/` 里的提议,等人确认 | opt-in beta;三个 M.O. 键里最重的一条——挂 `session/event` 且要**同步解压 zstd 会话日志**(本机 118 份实测 16.2s,单次预算 `budgetMs`=4s 兜底),故 `LIGHT_MODE_OFF` 里有它。**永不自动落草稿**:`materialize` 恒 false,进仓库必须是人的显式动作 |
@@ -348,6 +356,7 @@
348
356
  `dreamNarrativeEnabled` · `documentMemoryEnabled` · `heatEnabled` · `continuityRescueEnabled` ·
349
357
  `organizeEnabled`(#231) ·
350
358
  `profileExtractEnabled`(#497) ·
359
+ `profileFactsEnabled`(2026-11-06) ·
351
360
  `preInjectGate.enabled`(#380) · `distillRecords.enabled`(#399)
352
361
 
353
362
  预设只是默认值而非强制:用户显式写进 feature_flags 的值在装配顺序上后展开、仍然生效
@@ -18,6 +18,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
18
18
  import { homedir } from "node:os";
19
19
  import { TYPES } from "./store.js";
20
20
  import { bootstrapFromDirectory } from "./bootstrap.js";
21
+ import { parseBootstrapJson, readBootstrapDir, bootstrapReply } from "./bootstrap-http.js";
21
22
  import { PACKAGE_VERSION } from "./version-check.js";
22
23
  import { parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
23
24
  import { parseShareBody, shareReply, parseReceiveBody, receiveReply, parseUndoBody, undoReply } from "./share-http.js";
@@ -763,34 +764,29 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
763
764
  // --- POST /bootstrap: cold-start memories from repo files (#220) -------
764
765
  // 显式收 dir(插件没有工作区根目录概念,不猜路径);确定性解析零 LLM;
765
766
  // 幂等骑 saveWithDedupe 的 (type, title, scope) 去重 + _overwrite 刷新。
767
+ //
768
+ // 解析、校验、答复形状全走 bootstrap-http.js:#386 第 6 条给宿主内路由补了
769
+ // 第二张脸,按 #411 的纪律,两张皮只许剩鉴权与路径不同。这里刻意**不**再
770
+ // 自己 JSON.parse 一遍——真要写第二份,test/reclaim-http.test.js 那类静态锁
771
+ // 会转红,而漂移在两边都只表现为「某个错误码不一样了」。
766
772
  if (req.method === "POST" && pathname === "/bootstrap") {
767
773
  void readBody(req, res).then((text) => {
768
774
  if (text === null) return;
769
- let body;
770
- try {
771
- body = JSON.parse(text || "{}");
772
- } catch {
773
- sendJson(res, 400, { error: "invalid-json" });
774
- return;
775
- }
776
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
777
- sendJson(res, 400, { error: "invalid-body" });
775
+ const parsed = parseBootstrapJson(text);
776
+ if (!parsed.ok) {
777
+ sendJson(res, 400, { error: parsed.error });
778
778
  return;
779
779
  }
780
- if (typeof body.dir !== "string" || !body.dir.trim()) {
781
- sendJson(res, 400, { error: "missing-dir" });
780
+ const dirResult = readBootstrapDir(parsed.body);
781
+ if (!dirResult.ok) {
782
+ sendJson(res, 400, { error: dirResult.error });
782
783
  return;
783
784
  }
784
- bootstrapFromDirectory({ service, dir: body.dir, logger })
785
- .then((summary) => sendJson(res, 200, summary))
786
- .catch((error) => {
787
- if (error?.code) {
788
- sendJson(res, 400, { error: error.code });
789
- return;
790
- }
791
- logger?.warn?.(`[dsh-mneme] standalone API bootstrap failed: ${String(error)}`);
792
- sendJson(res, 500, { error: "internal" });
793
- });
785
+ bootstrapReply({
786
+ bootstrapper: ({ dir }) => bootstrapFromDirectory({ service, dir, logger }),
787
+ dir: dirResult.dir,
788
+ warn: (msg) => logger?.warn?.(msg)
789
+ }).then(({ status, body }) => sendJson(res, status, body));
794
790
  });
795
791
  return;
796
792
  }