@modusensus/dsh-mneme 0.8.4 → 0.8.5

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 (69) hide show
  1. package/README.md +16 -7
  2. package/bin/dsh-mneme-mcp.mjs +11 -3
  3. package/lib/api-standalone.js +11 -0
  4. package/lib/api.js +21 -0
  5. package/lib/client.js +213 -31
  6. package/lib/config.js +82 -0
  7. package/lib/document.js +221 -0
  8. package/lib/dream/sleep.js +6 -4
  9. package/lib/dream.js +1347 -1319
  10. package/lib/guide.js +44 -0
  11. package/lib/heat.js +2 -0
  12. package/lib/inject.js +91 -15
  13. package/lib/lang.js +6 -0
  14. package/lib/organize.js +450 -0
  15. package/lib/recall-stats.js +23 -1
  16. package/lib/runtime/layout.js +30 -0
  17. package/lib/runtime/loader.js +4 -1
  18. package/lib/runtime/verify.js +19 -6
  19. package/lib/search/adaptive.js +38 -0
  20. package/lib/service.js +165 -12
  21. package/lib/settings.js +31 -2
  22. package/lib/store.js +109 -8
  23. package/lib/summarize.js +253 -8
  24. package/lib/tools.js +136 -8
  25. package/package.json +6 -6
  26. package/runtime-manifest.json +104 -174
  27. package/scripts/build-runtime-manifest.mjs +8 -1
  28. package/scripts/sync-test-badge.mjs +18 -12
  29. package/src/api-standalone.js +11 -0
  30. package/src/api.js +21 -0
  31. package/src/config.js +82 -0
  32. package/src/document.js +221 -0
  33. package/src/dream/sleep.js +6 -4
  34. package/src/dream.js +1347 -1319
  35. package/src/guide.js +44 -0
  36. package/src/heat.js +2 -0
  37. package/src/inject.js +91 -15
  38. package/src/lang.js +6 -0
  39. package/src/organize.js +450 -0
  40. package/src/recall-stats.js +23 -1
  41. package/src/runtime/layout.js +30 -0
  42. package/src/runtime/loader.js +4 -1
  43. package/src/runtime/verify.js +19 -6
  44. package/src/search/adaptive.js +38 -0
  45. package/src/service.js +165 -12
  46. package/src/settings.js +31 -2
  47. package/src/store.js +109 -8
  48. package/src/summarize.js +253 -8
  49. package/src/tools.js +136 -8
  50. package/test/adaptive-inject-budget.test.js +85 -0
  51. package/test/api.test.js +65 -3
  52. package/test/client.test.js +63 -2
  53. package/test/document.test.js +523 -0
  54. package/test/dream.test.js +51 -0
  55. package/test/inject-content-cap.test.js +5 -3
  56. package/test/inject-pools.test.js +170 -0
  57. package/test/inject-recall.test.js +105 -0
  58. package/test/inject.test.js +35 -1
  59. package/test/llm-audit.test.js +153 -0
  60. package/test/organize.test.js +418 -0
  61. package/test/recall-stats.test.js +7 -1
  62. package/test/runtime-loader.test.js +18 -0
  63. package/test/runtime-manifest.test.js +2 -1
  64. package/test/runtime-verify.test.js +65 -0
  65. package/test/sleep.test.js +37 -0
  66. package/test/store.test.js +13 -0
  67. package/test/summarize.test.js +407 -3
  68. package/test/tools-dto-schema-matrix.test.js +325 -0
  69. package/test/tools.test.js +36 -3
package/README.md CHANGED
@@ -5,11 +5,11 @@
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-1160%20passed-success)](https://github.com/modusensus/dsh-mneme)
9
- [![CI](https://img.shields.io/github/actions/workflow/status/modusensus/dsh-mneme/ci.yml)](https://github.com/modusensus/dsh-mneme/actions)
8
+ [![tests](https://img.shields.io/badge/tests-1275%20passed-success)](https://github.com/slow-stack/dsh-mneme)
9
+ [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/dsh-mneme/ci.yml)](https://github.com/slow-stack/dsh-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)
12
- [![coverage](https://img.shields.io/codecov/c/github/modusensus/dsh-mneme/main)](https://codecov.io/gh/modusensus/dsh-mneme)
12
+ [![coverage](https://img.shields.io/codecov/c/github/slow-stack/dsh-mneme/main)](https://codecov.io/gh/slow-stack/dsh-mneme)
13
13
 
14
14
  > 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
15
15
 
@@ -234,6 +234,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
234
234
 
235
235
  | 版本 | 亮点 |
236
236
  |------|------|
237
+ | **v0.8.5** | 主动整理 + 注入形态 + 蒸馏可靠性批次:agent 主动整理接口 `service.organize`——`dryRun` 出比对报告(精确层标题归一,向量层 0.92 与 document / session 档同源)→ agent 判断 → `apply` 落库,筛除=归档不删、同一份报告只许落地一次、全程复用 `dream_runs` 回执(#231/#267,heptaspirit);注入形态第一批——`injectGuidanceEnabled` 能力说明(工具描述尾句 + order 150 常驻段)与 `pinnedInjectBudget` 约束/偏好 pin 池逐字保真(#249);注入预览卡 `/inject-preview` 旁路快照(#179);蒸馏游标持久化 `distill_cursors` 与旧宿主内存游标降级(#229/#274/#279);工具暴露开关 `disableMemorySearch` / `disableMemoryArchive`(#276);document 型记忆(#230)、总览独立路由(#258)、错峰队列(#239 第 4 项)、注入命中留痕(#217)、token 记账修复(#242)、运行完整性判据修复(#268);1275 测试全绿 |
237
238
  | **v0.8.4** | MCP 六件套 + 图召回轴 + 冷启动 + 注入截断/状态条 + 蒸馏可靠性批次:stdio MCP server 让记忆六件套进任意 MCP 客户端——零依赖(JSON-RPC 2.0 换行帧,不引 SDK),工具面与 `src/tools.js` 逐字对齐、平价回归锁漂移,配置沿用 CLI 约定(#181/#214);图召回轴 `entityRecallEnabled`——检索融合池三源扩四源,实体挂联记忆参与 blend/rrf/minmax,与 BM25 同为确认/回填信号(#219/#222);冷启动 `src/bootstrap.js` 从仓库文件反向构建初始记忆——零 LLM 必有产出、幂等,`POST /bootstrap`(#220/#223);注入截断上限可配 `injectContentMaxChars` + 截断尾部带全文 `memory_get` 指引(#164①/#225);dream 总览升级常驻状态条 + 叙述条 `dreamNarrativeEnabled`(#164/#227/#228);记忆复用统计卡 / 注入状态卡 / 路由旧值显式标记(#182/#217/#191/#213);heat 幂律换广义指数 `heatGlobalBeta`(#218);修复 reasoningEffort 连通性透传、summarize 增量蒸馏 + 子会话交付补齐(#215/#226/#232);1160 测试全绿 |
238
239
  | **v0.8.3** | 注入轮换与下载可靠性批次:注入位跨轮轮换 `injectRotationTurns`(默认 0 = 关)——同一会话相邻轮次不再反复注入同一条,窗口只看之前轮次、同查询工具轮不推进不转自己(sessionId 分桶 FIFO 32,#205/#206);候选池四处扩容 `poolSize = maxItems × (轮换窗口 + 1)`(下限 200),旋钮开多大、池子就够多大(#208);模型文件下载断点续传与重试 `resilientModelDownload`(默认开)——Range / If-Range 续传、416 / 偏移失配重置、单写者锁 + 降级直通 + 空闲看门狗(#194/#207);1079 测试全绿 |
239
240
  | **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 测试全绿 |
@@ -350,6 +351,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
350
351
  | **v0.8.3** | ✅ 完成 | 注入轮换 + 模型下载可靠性 | `injectRotationTurns` 跨轮轮换(默认 0 = 关)——相邻轮次注入去重,窗口只看之前轮次、同查询工具轮不推进不转自己;候选池四处扩容 `poolSize = maxItems × (窗口+1)` 配套,轮换越得出窗口(#205/#206/#208,Philia-FY 报告 + A/B + 池子盲区定位);`resilientModelDownload` 断点续传与重试(默认开)——Range 续传 / 失配重置 / 单写者锁 + 降级直通 / 空闲看门狗(#194/#207,heptaspirit);1079 测试全绿 |
351
352
 
352
353
  | **v0.8.4** | ✅ 完成 | MCP 六件套 + 图召回轴 + 冷启动 + 注入截断/状态条 + 蒸馏可靠性 | stdio MCP server 让记忆六件套进任意 MCP 客户端——零依赖(JSON-RPC 2.0 换行帧,不引 SDK)、工具面与 `src/tools.js` 逐字对齐、平价回归锁漂移,配置沿用 CLI 约定(#181/#214);图召回轴 `entityRecallEnabled`——检索融合池三源扩四源,实体挂联记忆参与 blend/rrf/minmax,与 BM25 同为确认/回填信号(#219/#222);冷启动 `src/bootstrap.js` 从仓库文件反向构建初始记忆——零 LLM 必有产出、幂等,`POST /bootstrap`(#220/#223);注入截断上限可配 `injectContentMaxChars` + 截断尾部带全文 `memory_get` 指引(#164①/#225);dream 总览常驻状态条 + 叙述条 `dreamNarrativeEnabled`(#164/#227/#228);记忆复用统计卡 / 注入状态卡 / 路由旧值显式标记(#182/#217/#191/#213);heat 广义指数 `heatGlobalBeta`(#218);reasoningEffort 连通性透传 + summarize 增量蒸馏与子会话交付(#215/#226/#232);1160 测试全绿 |
354
+ | **v0.8.5** | ✅ 完成 | 主动整理接口 + 注入形态 + 蒸馏可靠性 | agent 主动整理接口 `service.organize`(`dryRun` 比对报告 → agent 判断 → `apply` 落库;筛除=归档不删、同一份报告只许落地一次、全程复用 `dream_runs` 回执,#231/#267,heptaspirit);注入形态第一批——`injectGuidanceEnabled` 能力说明段与 `pinnedInjectBudget` 约束/偏好 pin 池(#249)、注入预览卡(#179);蒸馏游标持久化 `distill_cursors` 与旧宿主内存游标降级(#229/#274/#279);工具暴露开关 `disableMemorySearch` / `disableMemoryArchive`(#276);document 型记忆(#230);总览独立路由(#258);错峰队列(#239 第 4 项);1275 测试全绿 |
353
355
 
354
356
  > 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
355
357
 
@@ -379,7 +381,7 @@ dsh web
379
381
  #### 方式二:从源码安装
380
382
 
381
383
  ```bash
382
- git clone https://github.com/modusensus/dsh-mneme.git
384
+ git clone https://github.com/slow-stack/dsh-mneme.git
383
385
  cd dsh-mneme
384
386
  dsh plugin --profile web add .
385
387
  dsh web
@@ -416,6 +418,10 @@ dsh web
416
418
  | `summarizeProvider` / `summarizeModel` | 空 | 摘要的 LLM 路由覆盖(空=使用当前会话模型);推荐轻量模型节省主模型 token |
417
419
  | `summarizeMinIntervalMinutes` | `0` | autoSummarize 最小触发间隔(0-10080,0=不限):两次蒸馏之间最短间隔,失败/degraded run 也占用(#127) |
418
420
  | `summarizeMaxEntriesPerRun` | `0` | 单次蒸馏产出记忆条数上限(0-50,0=不限):节流防单会话大量重复条目(#127) |
421
+ | `summarizeMinWindowChars` | `0` | 蒸馏前零 LLM 预判:窗口可蒸馏文本不足此字符数直接跳过调用(0-100000,0=关);skip 原因写入审计(#239) |
422
+ | `summarizeMaxRunsPerSession` | `0` | 每会话最多发起多少次蒸馏 LLM 调用(0-1000,0=不限);只计真实调用,被预判拦下的不占额度(#239) |
423
+ | `summarizePeakHours` | 空 | 高峰时段(本地时间,逗号分隔、可带星期前缀,支持跨零点):空=关;命中时蒸馏顺延到非高峰、窗口累积后一次蒸、skip 原因入审计。例:按高峰计费的供应商可写 `mon-fri 08:00-12:00,14:00-18:00`(如 DeepSeek,以其官方定价页为准)(#239) |
424
+ | `summarizePeakMaxDeferMinutes` | `120` | 高峰顺延上限(0-1440 分钟,0=不设上限):到点仍处高峰就照常跑,避免长高峰把蒸馏饿死(#239) |
419
425
  | `summarizeDedupeMode` | `off` | 落库前去重档位:`off`(默认=现状)/ `title`(零成本,仅拦完全同名)/ `vector`(复用 embedding 列做同会话语义近邻,无 LLM 调用,#127) |
420
426
  | `summarizeDedupeMinSim` | `0.92` | vector 去重档的相似度阈值(0.5-0.99) |
421
427
  | `summarizeDedupeWindowHours` | `24` | vector 去重的同会话时间窗(小时,0-168) |
@@ -424,6 +430,7 @@ dsh web
424
430
  | `distillRateLimitRetries` | `3` | 命中 429 限流时的指数退避重试次数(0-10) |
425
431
  | `distillRateLimitBaseDelayMs` | `1000` | 429 退避基准延迟(ms):1s→2s→4s… |
426
432
  | `maxInjectedItems` | `5` | 最多注入几条记忆 |
433
+ | `injectUncertaintyAdaptive` | `false` | 注入条数的查询自适应:确定性强的话题把条数收缩到一半(下限 1),模糊话题(回指/时间线索,或极短查询)维持 `maxInjectedItems` 上限;**只做单向收缩**,判据只看查询本身、不做额外检索(#239 第 5 项) |
427
434
  | `injectRotationTurns` | `0` | 注入位跨轮轮换:同一条记忆在最近 N 个查询轮次注入过后本轮不再优先(新鲜优先、不足回填,槽位数不变;会话边界自动重置;`0` = 关闭保持现状) |
428
435
  | `injectContentMaxChars` | `300` | 注入单条正文截断上限(60-4000,原硬编码 300,#164①/#225):截断尾部带上限/原长/全文 `memory_get` 指引;块预算 `Math.max(1500, 上限+600)` 随上限放大 |
429
436
  | `importanceThreshold` | `3` | 注入的最低重要性(1-5) |
@@ -443,6 +450,8 @@ dsh web
443
450
  | `dreamImplicitKeep` | `true` | 显式决策覆盖率不足时的隐式 keep(未提及条目保持原样);`false` + `dreamMinExplicitCoverage: 0` 恢复旧严格行为 |
444
451
  | `dreamMinExplicitCoverage` | `0.5` | 显式决策覆盖率下限(0-1):合法子集低于该值降级 degraded 而非整单拒绝(#104 方向 1,PR #200) |
445
452
  | `dreamMaxSnapshotSize` | `200` | autoDream 滑动窗口上限:每次只对最近 N 条做 consolidation,窗口外不进 snapshot(防 LLM 输入撑爆) |
453
+ | `dreamSummaryProvider` / `dreamSummaryModel` | 空 | 总览(dream_summarize)专用模型路由(留空 = 沿用 `dreamProvider`/`dreamModel`)。consolidate 有窗口(`dreamMaxSnapshotSize`)而总览输入随库增长,ctx 需求差数倍——用小 ctx 模型跑巩固时把总览指到大 ctx 模型(issue #258) |
454
+ | `dreamSummaryMaxInputs` | `0` | 总览输入条数硬上限(0 = 不设上限):超过时按 `updated_at` 倒序只保留最新 N 条进总览,防小 ctx 模型被全库输入撑爆;总览口径脚注的条数随实际输入变化 |
446
455
  | `dreamMinIntervalMinutes` | `0` | autoDream 最小触发间隔(0-10080,0=不限):失败/degraded run 也占用 |
447
456
  | `dreamNarrativeEnabled` | `false` | 叙述条总开关(#164 对齐,v0.8.4):按共享 tag 主题簇合成叙述 + evidence 证据链,注入候选排除(按需检索,常驻位只留 dream 总览);也走 feature_flags 白名单,lightMode 强制关 |
448
457
  | `dreamNarrativeMinCluster` | `3` | 主题簇合成叙述的最小成员数(2-20,v0.8.4) |
@@ -516,7 +525,7 @@ dsh web
516
525
  | `sleepMaxPatternPerRun` | `3` | 每轮模式记忆产出上限(0=禁用,0-10) |
517
526
  | `sleepProvider` / `sleepModel` | 空 | sleep 深维护专用 LLM 路由覆盖(留空用巩固模型或当前模型;建议同巩固模型选非思考模型) |
518
527
  | `sleepReasoningEffort` | 未配置=最低档 | sleep 各阶段 LLM 推理档位(同 dreamReasoningEffort 语义,v0.7.26) |
519
- | `recallRecordDefault` | `true` | recall_runs 记录默认开(显式传 `recordRecall:false` 的调用方不受影响) |
528
+ | `recallRecordDefault` | `true` | recall_runs 记录默认开(显式传 `recordRecall:false` 的调用方不受影响);注入同样落账(`mode='inject'`,随本开关,#217) |
520
529
  | `recallRetentionDays` | `90` | recall_runs 滚动清理保留天数 |
521
530
  | `hotMemoryRounds` | `5` | 会话级短期热记忆轮次(v0.5.0):最近 N 轮对话滚动注入,从会话事件日志无状态重建、不落库 |
522
531
  | `hotMemoryMaxTokens` | `2000` | 热记忆 token 预算(v0.5.0,200-32000),超出滚动截断 |
@@ -676,7 +685,7 @@ src/
676
685
  ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
677
686
  └── index.js # 插件接线
678
687
  lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
679
- test/ # 1160 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
688
+ test/ # 1275 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
680
689
  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 运行时清单
681
690
  ```
682
691
 
@@ -685,7 +694,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
685
694
  ```bash
686
695
  cd dsh-mneme
687
696
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
688
- npm test # 运行 1160 个测试
697
+ npm test # 运行 1275 个测试
689
698
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
690
699
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
691
700
  ```
@@ -145,12 +145,17 @@ const previewOf = (content) => {
145
145
 
146
146
  const MEMORY_TYPE_ENUM = ["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"];
147
147
 
148
+ // #230:memory_list 的过滤枚举比 save/update 宽一档——document 指针行可被
149
+ // 列出(agent 需要按类型找到注册过的外档),但不进 save/update(铸造口唯一
150
+ // = registerDocument;MCP 暂不镜像该工具,接口面归 #231)。
151
+ const MEMORY_TYPE_LIST_ENUM = [...MEMORY_TYPE_ENUM, "document"];
152
+
148
153
  const TOOL_SAVE_DESCRIPTION =
149
154
  "Persist one memory entry for future sessions (user preferences, project state, decisions). " +
150
155
  "Call this when the user states a durable preference, a project decision is made, or a lesson is learned. " +
151
156
  "Merges into an existing entry of the same type when the title matches.";
152
157
  const TOOL_SEARCH_DESCRIPTION =
153
- "Search the cross-session memory store. Use when you need past context: how a problem was solved, user preferences, project decisions. Substring-matches title/content/tags, and augments results with semantic (vector) recall + optional rerank when an embeddings provider is configured. Returns matching entries with source and timestamps.";
158
+ "Search the cross-session memory store for past context (solved problems, user preferences, project decisions). IMPORTANT: relevant cross-session memories are already injected into your context every turn — only call this when the injected memory block does NOT contain what you need, or you need to look something specific up. Substring-matches title/content/tags, and augments results with semantic (vector) recall + optional rerank when an embeddings provider is configured. Returns matching entries with source and timestamps.";
154
159
  const TOOL_LIST_DESCRIPTION =
155
160
  "List memory entries by type, high-importance first, then newest, paginated. Set include_archived=true to also list archived (hidden) entries so they can be located and restored.";
156
161
  const TOOL_GET_DESCRIPTION =
@@ -240,7 +245,10 @@ async function runGet(config, args) {
240
245
  assertOk(res, [200]);
241
246
  const m = res.json;
242
247
  if (!m || !m.id) throw new Error("memory not found");
243
- return `${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}${MEMORY_PROVENANCE(m)}\n\n${m.content}`;
248
+ // #230:document 指针行把文件路径亮在首行,与 src/tools.js 的 memory_get
249
+ // render 逐字对齐。
250
+ const doc = m.doc_path ? ` | doc: ${m.doc_path}` : "";
251
+ return `${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}${doc}${MEMORY_PROVENANCE(m)}\n\n${m.content}`;
244
252
  }
245
253
 
246
254
  async function runUpdate(config, args) {
@@ -320,7 +328,7 @@ export const MCP_TOOLS = [
320
328
  inputSchema: {
321
329
  type: "object",
322
330
  properties: {
323
- type: typeEnumSchema("Filter by type; omit for all"),
331
+ type: { type: "string", enum: MEMORY_TYPE_LIST_ENUM, description: "Filter by type; omit for all. 'document' (#230) = agent-registered document pointer rows" },
324
332
  limit: intSchema("Page size (default 50)"),
325
333
  offset: intSchema("Page offset (default 0)"),
326
334
  include_archived: boolSchema("Include archived (hidden) entries so they can be found and restored (default false)"),
@@ -166,6 +166,11 @@ async function handlePutBody(res, service, logger, id, text) {
166
166
  sendJson(res, 404, { error: "not-found" });
167
167
  return;
168
168
  }
169
+ if (patch.type === "document" && existing.type !== "document") {
170
+ // #230 写入权分离:type 不许改入 document(铸造口唯一),干净 400。
171
+ sendJson(res, 400, { error: "document-requires-register" });
172
+ return;
173
+ }
169
174
  // 内容被改写时旧版本先入档(human_override),与内部面板写路径一致。
170
175
  if (patch.content !== undefined && patch.content !== existing.content) {
171
176
  const history = Array.isArray(existing.content_history) ? existing.content_history : [];
@@ -353,6 +358,12 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
353
358
  sendJson(res, 400, { error: "invalid-type" });
354
359
  return;
355
360
  }
361
+ if (body.type === "document") {
362
+ // #230 写入权分离:document 行铸造口唯一(registerDocument)。
363
+ // 这里给干净 400,不让它落进 saveWithDedupe 的守卫变 500。
364
+ sendJson(res, 400, { error: "document-requires-register" });
365
+ return;
366
+ }
356
367
  if (typeof body.title !== "string" || !body.title.trim()) {
357
368
  sendJson(res, 400, { error: "missing-title" });
358
369
  return;
package/lib/api.js CHANGED
@@ -5,6 +5,7 @@ import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
5
5
  import { langOf } from "./lang.js";
6
6
  import { computeHeat } from "./heat.js";
7
7
  import { describeStreamFailure, resolveRoute } from "./dream.js";
8
+ import { getInjectionSnapshot } from "./inject.js";
8
9
  import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
9
10
  import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
10
11
  import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
@@ -1336,6 +1337,26 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
1336
1337
  }
1337
1338
  });
1338
1339
 
1340
+ // 注入预览(#179,只读):最近一帧 systemPrompt 组装的构成与体积——快照由
1341
+ // inject.js 在真实渲染路径旁路缓存(同一份 candidates,不二次检索)。null =
1342
+ // 尚未发生过渲染(新宿主/新会话)或 autoInject 关闭(注入器未注册),面板对
1343
+ // 两者都以「暂无预览」呈现,不区分也不猜。
1344
+ register({
1345
+ kind: "exact",
1346
+ path: "/api/dsh-mneme/inject-preview",
1347
+ handler(req, res) {
1348
+ try {
1349
+ if (req.method !== "GET") {
1350
+ sendJson(res, 404, { error: "not-found" });
1351
+ return;
1352
+ }
1353
+ sendJson(res, 200, { snapshot: getInjectionSnapshot() });
1354
+ } catch {
1355
+ sendJson(res, 500, { error: "internal" });
1356
+ }
1357
+ }
1358
+ });
1359
+
1339
1360
  // --- custom commands ---
1340
1361
  register({
1341
1362
  kind: "exact",