@modusensus/dsh-mneme 0.9.4 → 0.9.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.
- package/README.md +53 -10
- package/bin/cli.mjs +215 -2
- package/docs/CONFIGURATION.md +4 -2
- package/docs/DAEMON.md +9 -2
- package/docs/DISTILL.md +66 -4
- package/docs/SHARE.md +552 -0
- package/docs/SLEEP.md +1 -1
- package/docs/STORAGE.md +1 -1
- package/lib/api-standalone.js +286 -8
- package/lib/api.js +195 -1
- package/lib/client.js +783 -30
- package/lib/document.js +15 -2
- package/lib/dream/decisions.js +88 -3
- package/lib/dream/sleep.js +63 -13
- package/lib/dream.js +65 -9
- package/lib/lang.js +23 -6
- package/lib/memory-source.js +9 -0
- package/lib/organize.js +5 -1
- package/lib/pipeline.js +62 -1
- package/lib/provenance-http.js +126 -0
- package/lib/serve.js +13 -0
- package/lib/service.js +67 -4
- package/lib/share-http.js +281 -0
- package/lib/share-import.js +234 -0
- package/lib/share.js +335 -0
- package/lib/store.js +272 -11
- package/lib/summarize.js +60 -11
- package/package.json +1 -1
- package/src/api-standalone.js +286 -8
- package/src/api.js +195 -1
- package/src/document.js +15 -2
- package/src/dream/decisions.js +88 -3
- package/src/dream/sleep.js +63 -13
- package/src/dream.js +65 -9
- package/src/lang.js +23 -6
- package/src/memory-source.js +9 -0
- package/src/organize.js +5 -1
- package/src/pipeline.js +62 -1
- package/src/provenance-http.js +126 -0
- package/src/serve.js +13 -0
- package/src/service.js +67 -4
- package/src/share-http.js +281 -0
- package/src/share-import.js +234 -0
- package/src/share.js +335 -0
- package/src/store.js +272 -11
- package/src/summarize.js +60 -11
- package/test/api.test.js +370 -2
- package/test/audit.test.js +2 -2
- package/test/cli-receive.test.js +390 -0
- package/test/cli-share.test.js +437 -0
- package/test/client-share.test.js +225 -0
- package/test/client.test.js +139 -0
- package/test/dedupe-candidate-window.test.js +180 -0
- package/test/dream-candidate-hybrid.test.js +2 -2
- package/test/dream-fence.test.js +164 -0
- package/test/dream.test.js +87 -5
- package/test/helpers/dream-mock.js +6 -2
- package/test/llm-message-source.test.js +1 -1
- package/test/mint-source.test.js +1 -1
- package/test/pipeline-r1r2.test.js +129 -0
- package/test/policy-epoch.test.js +2 -2
- package/test/provenance-http.test.js +184 -0
- package/test/readme-claims.test.js +45 -0
- package/test/reasoning-effort.test.js +10 -10
- package/test/scope-candidates.test.js +7 -4
- package/test/scope-state.test.js +130 -1
- package/test/serve.test.js +62 -0
- package/test/share-foreign-rows.test.js +144 -0
- package/test/share-http.test.js +144 -0
- package/test/share-import.test.js +577 -0
- package/test/share-imported-guard.test.js +311 -0
- package/test/share-imported-identity.test.js +111 -0
- package/test/share-pack.test.js +511 -0
- package/test/share-receive-http.test.js +756 -0
- package/test/share-routes.test.js +337 -0
- package/test/share-store.test.js +133 -0
- package/test/share-validate.test.js +101 -0
- package/test/standalone-api.test.js +524 -5
- package/test/summarize-parse-diagnostics.test.js +75 -0
- package/test/summarize.test.js +15 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/slow-stack/mneme)
|
|
9
9
|
[](https://github.com/slow-stack/mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -49,7 +49,8 @@ dsh web
|
|
|
49
49
|
- 逐 type 记录 `committed / failed / pending` 回执,健康端点区分 `ok / degraded / unknown`
|
|
50
50
|
- 状态写失败不静默:同步失败落日志并留债务,重启自动收敛
|
|
51
51
|
|
|
52
|
-
###
|
|
52
|
+
### 模型工具(默认注册 11 个;开 `organizeEnabled` 后 12 个)
|
|
53
|
+
|
|
53
54
|
|
|
54
55
|
| 工具 | 功能 |
|
|
55
56
|
|------|------|
|
|
@@ -61,7 +62,9 @@ dsh web
|
|
|
61
62
|
| `memory_delete` | 删除记忆(按记忆 ID 精确删除) |
|
|
62
63
|
| `memory_forget` | 抑制注入(降权不删除,可恢复) |
|
|
63
64
|
| `memory_archive` | 归档/恢复记忆(v0.2.5;归档后隐藏于列表/搜索/注入/整理,`archived=false` 可恢复) |
|
|
65
|
+
| `set_scope_state` | 按下/松开某个作用域的**可逆闸门**(#471:政策轴。暂停期间该作用域的记忆照旧存着,只是不注入、不参与巩固,松开即原样回来;不改任何一条记忆的标注,与上面逐条操作的 `memory_forget` / `memory_archive` 是两根不同的轴)。两级开关 `scopeState.enabled` / `.enforce` 默认都关,没开时按下不产生拦截效果 |
|
|
64
66
|
| `memory_register_document` | 登记 agent 产长文档的指针行(#230:摘要 + `doc_path` + evidence 落 `document` 类型;正文留在磁盘、由 agent 自管,注册口唯一,旁路写入被拒) |
|
|
67
|
+
| `memory_organize` | 主动整理(#231,**要 `organizeEnabled` 打开才注册**):`mode=dryRun` 先出比对报告(每条候选 `exact` / `near` / `new` + 命中行,**不写一行库**、只留审计回执),agent 判断后 `mode=apply` 引用那份报告的 `run_id` 落地。两种「不要它」要分清——**`discard`** = 这条候选不写,什么都不动(只进回执);**`archive`** = 把报告标为被取代的那条**既有行**归档(可恢复,绝不物理删除)。没有比对过的候选写不进库,一份报告也只许落地一次 |
|
|
65
68
|
| `memory_runtime` | 本地推理运行时的状态 / 供给 / 校验(v0.7.32,自管运行时三档取件) |
|
|
66
69
|
|
|
67
70
|
### 工作流(M.O.,v0.9 beta)
|
|
@@ -254,6 +257,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
254
257
|
|
|
255
258
|
图谱 ↔ 记忆双向互跳:图谱详情侧的关联记忆可点击,记忆边的「来源记忆」按 memory_id 直跳三栏视图并自动定位。
|
|
256
259
|
|
|
260
|
+
- **分享包三步(面板)**:「⋯」菜单里的**分享记忆包…**(报告先于下载:进包条数、同键折叠、六个跳过原因、可展开的跳过清单,超水位回 413 同屏)与**导入分享包…**(选文件 → 干跑预览将新建几条 / 撞名几条 → 确认才写库),外来行带 `from-<别名>` 徽标、工具条「外来」chip 按 `source=imported` 服务端筛、抽屉「收起这个包」两段式整包退出(先清点后物删)。口径与判据一条都不在面板里复述——出包在 `src/share.js`、入包闸门在 `src/share-import.js`、协议层在 `src/share-http.js`,细则与实测见 [docs/SHARE.md](docs/SHARE.md)
|
|
261
|
+
|
|
257
262
|
### 四路召回融合与会话热记忆 🔎(v0.5.0,v0.8.4 扩四路)
|
|
258
263
|
|
|
259
264
|
- **BM25 稀疏第三路召回**(`src/search/bm25.js`):与向量召回、FTS5/LIKE 关键词并列——ASCII 词元 + CJK bigram 分词、IDF 加权(归一化 [0,1]),专有名词 / ID / 代码片段等散词查询不再依赖子串命中。融合规则:未召回行按 `0.3×BM25分` 回填;仅向量召回行获得词法加分;LIKE 已命中行不叠分。`bm25SearchEnabled` 可关
|
|
@@ -276,6 +281,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
276
281
|
| **v0.9.2**(2026-10-19) | 让「看不见的失败」显形:功能开关的第三态「待重启」把「面板显示已改 ≠ 运行时已改」摆到界面上(#481);作用域级可逆闸门让一路记忆能被单独按下与松开、不必改配置重启(#471,默认关);巩固用的 `dreamMaxTokens` 默认值撞服务商上限、八天 180 次全失败,面板现在能改、超上限还会自动降级重试(#478);CLI 把 `--` 开头的值当新旗标的真实缺陷(一次 1/4096 概率的 CI 红揪出来的)、人格卡删除 × 永远不可见、自动蒸馏写入的行 100% 拿不到向量(#477) |
|
|
277
282
|
| **v0.9.3**(2026-10-20) | M.O. 从「挖得出来」到「会被叫去挖」:沉淀链算法自 v0.9.1 起就完整实现(多帧 zstd → 重复序列 → 缺席对照 → draft 草稿),运行时却没有任何调用点——机器挖得出来、却永远不会被叫去挖。这一版把归属与触发接上:按会话头 `cwd` 反查项目桶(桶名是路径的有损变形,查不到整跑跳过而不是降级成全局普查)、挂 `turn/end`、冷却 10 分钟、单次预算 4 秒、延到宏任务、一次一个项目、抛错只 `warn`,**只产提议不产草稿**(`actionNudgeEnabled`,beta,默认关);面板加「现在沉淀一次」,与自动跑同一条入口,区别只有它不改仓库;另修作用域反查自 v0.8.0 起每次都抛错走降级(#487)、`/nudge` 三处「没看全」被报成「确实没有」(#489 评审)、多帧 zstd 反证断言绑了 Node 整块解语义(#490),并把整个 M.O. 的「手法」改回「工作流」 |
|
|
278
283
|
| **v0.9.4**(2026-10-21) | 三条链各自补上「最后一环」——东西早就造好了,只差没人能叫它:**agent 主动整理**(`memory_organize`,先 dryRun 出比对报告 → agent 判断 → apply 落库,筛除只归档不删、全程留审计回执;`organizeEnabled` 默认关,关时工具根本不注册);**画像提取草稿**(面板按钮读活跃记忆 + 一次模型调用,产一段「助手眼中的用户」草稿,`profileExtractEnabled` 默认关)——**只出草稿**,采用是用户再点一次,手写的画像原文不会被模型改写,空库与解析失败都直接报错、绝不回空草稿冒充「没提炼出东西」;**人格寻址收口**(`default` 卡以前无条件落给所有没建卡的身份,宿主里自带身份的 bot 每帧收到两句「你是谁」;`personaStrictAddressing` 默认关,把「没建卡」与「卡被显式关掉」都收回为不注入)。同版补蒸馏的**转录围栏 + 解析失败护栏**(#494:一条会话被同一窗口卡死、重试 20 次越试越贵的根因;失败行现在恒带原始输出形状),把面板自称「注入生效文本」的只读区改成后端单源渲染(并修它从不显示人格行),评审另收掉两处「坏了却不报」:整理报告侧的存在性探测口(`strictScope` 下按调用方可见性过滤落点并重算计数)、画像保存不判 `res.ok` 却提示已保存 |
|
|
284
|
+
| **v0.9.5**(2026-10-30) | 记忆能安全地递给别人,也能整包收回来:**分享包 `mneme.share/v1`**(`dsh-mneme share` / `receive` / `undo-share`,面板「⋯」里同一批三件事)只含活跃行,`agent_scope` / `workspace_scope` 与本机 home 前缀一律不进包,命中密钥 / PII 的行**整条不进**(不打码);出包先给报告(进包数 / 折叠数 / 六个跳过原因)再给文件,3 MiB 水位量在紧凑序列化的字节上,超限回 413 与同一份报告,分批靠 `--type` / `--tag`;入包一条不合格**整包拒**、干跑是默认、按 `share_external_id` 幂等,整包退出是物删而不是归档(归档会占住去重键,重导即撞)。「本机巩固不许改写别人导入的原文」由单一谓词 `isImportedRow` 守在 `saveWithDedupe` 汇流口与九个行池上。同版给记忆来源加了读出口 `GET /memory/provenance`(#421,@z2Ace0107 实现,依附既有 `distillRecords.enabled`:409 = 用户关了、503 = 装配坏了),修掉同标题记忆被重复写第二份(#505)、模式扫描谎报「没东西可扫」(#515)、五处巩固 prompt 裸放导致的 46K token 空转(#501 / #493)与 dream 合并护栏的整类误拦(#521),面板新增设置页粘顶分组目录(#518)与宿主极简预设的在场说明(#514) |
|
|
279
285
|
|
|
280
286
|
| **v0.8.x** | 作用域隔离(agent / workspace 双维 + opt-in 硬过滤)、冲突队列人工裁决、生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断) |
|
|
281
287
|
|
|
@@ -501,7 +507,7 @@ dsh plugin --profile web add "C:/path/to/modusensus-dsh-mneme-0.9.0.tgz"
|
|
|
501
507
|
| `conflictFreezeMaxPending` | `100` | 冲突冻结队列最大挂起数(1-1000):超出后不再入队(防队列无限膨胀) |
|
|
502
508
|
| `escapePromptVariables` | `true` | 注入文本花括号转义(v0.8.0 恢复,issue #162):注入边界把 `{{...}}` 转义,防止 hot memory / 记忆原文里的 Go template / Vue 语法触发宿主 interpolate 抛错卡死会话(v0.7.4 曾修复、v0.7.11 误删) |
|
|
503
509
|
|
|
504
|
-
> 🔐 **API 安全**:DSH 无内置鉴权且默认仅监听 `127.0.0.1`。插件 API 默认开放(便于 Web 面板即装即用)——**未配 `apiToken` 时读和写都开放**,真正的围栏是回环 + 宿主监听器的 Host/Origin 校验(它防 DNS rebinding 与跨站浏览器请求,但不建立身份)。如需防护(如局域网暴露),在配置中设置 `apiToken
|
|
510
|
+
> 🔐 **API 安全**:DSH 无内置鉴权且默认仅监听 `127.0.0.1`。插件 API 默认开放(便于 Web 面板即装即用)——**未配 `apiToken` 时读和写都开放**,真正的围栏是回环 + 宿主监听器的 Host/Origin 校验(它防 DNS rebinding 与跨站浏览器请求,但不建立身份)。如需防护(如局域网暴露),在配置中设置 `apiToken`:此时**写操作**(新增/更新/删除/导入/收包(`/receive`、`/receive/undo`——写的是**本机自己的库**,归 `token-write`,所以免 token 的 GET 在那两条路径上只会拿到 405)/画像/规则/命令/开关/开关计费类)与**敏感读**(`/export`、`/external-api`、`/vector-config`、`/vector-reindex`、`/maintenance/*`、`/runtime/provision`、`/share`、`/memory/provenance`(蒸馏窗口的逐条会话原文,另受 `distillRecords.enabled` 门控))需携带 `Authorization: Bearer <token>`(前端设置面板填同一个 token);只读的 `list` / `search` / `semantic` / `inject-preview` 等**仍然开放**,不要把它们当成被 token 保护的面。`/api/dsh-mneme/vector-config` 返回的 `apiKey` 已掩码(`sk-***…`),存储仍保留明文供调用;前端回传空或掩码值表示"不改 key"。
|
|
505
511
|
>
|
|
506
512
|
> 安全模型全表(哪些路由免鉴权、为什么)见 [SECURITY.md](../SECURITY.md#5-security-model-what-actually-protects-what)。
|
|
507
513
|
|
|
@@ -527,14 +533,36 @@ dsh plugin --profile web add "C:/path/to/modusensus-dsh-mneme-0.9.0.tgz"
|
|
|
527
533
|
| `GET` | `/profile` | 用户画像,返回 `{profile:"..."}`;未设置时为空字符串 |
|
|
528
534
|
| `GET` | `/rules` | 行为规则列表,返回 `{rules:[...]}`;未设置时为空数组 |
|
|
529
535
|
| `GET` | `/memories?limit&offset&type&minImportance&source&order=chrono&include_archived&occurred_from&occurred_to` | 分页列出记忆(`include_archived=true` 含归档;occurred 时间窗按 `occurred_at` 回退 `created_at`) |
|
|
536
|
+
| `GET` | `/memory/provenance?id=<memoryId>` | 记忆级溯源读出口(#399 写、#421 读;宿主内面板路由 `/api/dsh-mneme/memory/provenance` 同一口径):一条溯源链接一个窗口 `{session_id, seq_start, seq_end, created_at, records[]}`,`records` 是那段蒸馏窗口的**逐条会话原文**(按 `seq` 升序、不越出区间)。没有链接的记忆返回**空 `windows`**——「不是蒸馏来的」是有效答案,不是错误。状态码阶梯:`401` 无 token → `400` 缺 `id` → `409 {"error":"distill-records-disabled"}`(`distillRecords.enabled` 关着;该开关是**启动快照**,面板改完要到下次启动生效,面板会显示「待重启」)→ `503 {"error":"provenance-unavailable"}`(装配没接线)→ `200`。`409` 与 `503` 必须分开读数:前者是「用户关了」,后者是「装配坏了」。**关开关 ≠ 删掉已留存原文**——只是不再经 HTTP 提供,盘上的行要 `dsh-mneme reclaim`(或重新开启后的启动裁剪)才走,详见 [docs/DISTILL.md](docs/DISTILL.md) |
|
|
530
537
|
| `GET` | `/memories/:id` | 单条记忆 |
|
|
531
538
|
| `POST` | `/memories` | 新增记忆 `{type,title,content,importance?,tags?,source?,sensitivity?,occurred_at?,agent_scope?,workspace_scope?}`;响应含 `action: created\|merged` |
|
|
532
539
|
| `PUT` | `/memories/:id` | 局部更新 `{title?,content?,type?,importance?,tags?,reason?,agent_scope?,workspace_scope?}`;content 改写按 human_override 入档 |
|
|
533
540
|
| `DELETE` | `/memories/:id` | 删除记忆 |
|
|
534
541
|
| `GET` | `/search?q&mode=keyword\|vector\|hybrid\|auto&topK&occurred_from&occurred_to&agent_scope&workspace_scope` | 搜索(关键词 / 向量 / hybrid=向量领位关键词补位 / 自动)。scope 两参声明调用方身份(不传 = 不解析,行为与旧版一致;任一给出则 strictScope 硬过滤与 A2 软加权随之生效) |
|
|
535
|
-
| `GET` | `/context?q&topK&threshold&agent_scope&workspace_scope` | 注入候选一站式聚合(issue #370,#363 承诺):宿主注入管线 `injectCandidates` 全语义(优先级分层 / 编码门控 / heat / scope 软加权 / hybrid 语义路 / pin 前置)+ 画像 + 规则,返回 `{profile, rules, items, pinnedCount}` 结构化条目,不含渲染文本。`threshold` 是 **importance 阈值**(默认 3,与 `/search` 的相似度阈值同名不同义);`topK` 默认 5(对齐宿主 `maxInjectedItems`);scope 语义与 `/search` 同口径;跨轮轮换 / 热记忆 / 时间前缀不含(调用方自管);recall_runs 注入分账与宿主注入同口径(#217 `mode='inject'`) |
|
|
542
|
+
| `GET` | `/context?q&topK&threshold&agent_scope&workspace_scope` | 注入候选一站式聚合(issue #370,#363 承诺):宿主注入管线 `injectCandidates` 全语义(优先级分层 / 编码门控 / heat / scope 软加权 / hybrid 语义路 / pin 前置)+ 画像 + 规则,返回 `{profile, rules, items, pinnedCount, tier}` 结构化条目,不含渲染文本。`threshold` 是 **importance 阈值**(默认 3,与 `/search` 的相似度阈值同名不同义);`topK` 默认 5(对齐宿主 `maxInjectedItems`);scope 语义与 `/search` 同口径;跨轮轮换 / 热记忆 / 时间前缀不含(调用方自管);recall_runs 注入分账与宿主注入同口径(#217 `mode='inject'`) |
|
|
536
543
|
| `POST` | `/bootstrap` | 冷启动(v0.8.4):从仓库目录反向构建初始记忆,body `{dir}` 必填;幂等((type,title,scope) 去重 + `_overwrite` 原地刷新),零 LLM、必有产出 |
|
|
537
|
-
| `POST` | `/maintenance/reclaim` | 无损回收(#275 / #411,与面板同口径):**只有 POST
|
|
544
|
+
| `POST` | `/maintenance/reclaim` | 无损回收(#275 / #411,与面板同口径):**只有 POST**(本路径不提供 GET 预估分支)。字段只从 JSON body 读;`dryRun` 默认 `true`,只有 `dryRun:false` 且 `confirm:true` 才真删;`vacuum` 只允许显式指定 |
|
|
545
|
+
| `POST` | `/share` | 记忆分享包:把活跃记忆按口径打成可外发的 JSON 包,纯读不写库;取行与跳过口径、脱敏规则与 3 MiB 体积水位见 [docs/SHARE.md](docs/SHARE.md)。本路径的 GET 一律 405 |
|
|
546
|
+
| `POST` | `/receive` | 吃进一份分享包(body `{pack, confirm?}`,包内容整个送上来、服务端不读路径):**默认干跑**,只有 `confirm: true` 才写库;整包一个事务、按 `share_external_id` 幂等(同一个包重跑 `created = 0`),外来行标 `source=imported`,不参与本机巩固/降级。包里一条不合格就**整包拒**(收侧不做逐条修复,也不跑敏感扫描)。入站上限 4 MiB;GET 一律 405。细则与实测见 [docs/SHARE.md](docs/SHARE.md) |
|
|
547
|
+
| `POST` | `/receive/undo` | 整包退出(body `{packId, confirm?}`,同样**默认干跑**只清点):按 `share_pack_id` **物删**(不是归档——归档行会占住 UNIQUE 位,撤了反而导不回来),`deleted` 是全量、`sample` 只给前 20 条预览;什么都没导入时答 0,不抛。GET 一律 405 |
|
|
548
|
+
|
|
549
|
+
#### `/context` 的 `tier` 档位信号(#383)
|
|
550
|
+
|
|
551
|
+
`GET /context` 的响应多一个 `tier` 字段,回答「这次注入跑在哪一档」。嵌入降级在调用方视角原本完全静默——`items` 看着正常,只是召回质量悄悄变差,外部集成方无从感知自己长期跑在降级档上。
|
|
552
|
+
|
|
553
|
+
| `tier` | 含义 |
|
|
554
|
+
|--------|------|
|
|
555
|
+
| `full` | 语义档:查询嵌入成功,且与库里的向量维度可比 |
|
|
556
|
+
| `degraded` | 规则档:语义路本次不可用(原因见日志) |
|
|
557
|
+
| `unknown` | 语义路未启用(`q` 为空),或判不出来(维度探针没跑成 / 一条向量都读不出)——那不是降级,但也不宣称 `full` |
|
|
558
|
+
|
|
559
|
+
**两套消费者的分工:** `tier` 是**响应读数**,给调用方——三态、fail-closed(判不出来报 `unknown`,不报 `full`),用于在集成侧告警或切换策略。降级的**具体原因**是**日志读数**,给运维——走 daemon 的 stderr(`dsh-mneme: /context degraded to the rule tier (<reason>)`),不进响应体。原因枚举六值,互不吞并(成例是「修法不同就分开」):`embed-unavailable`(无可用嵌入接口:embedder 对象在,上面没有 `embedSingle` / `embed`)/ `embed-empty`(返回了空或非数组向量)/ `embed-timeout`(接口挂起,被超时护栏按缺失处理)/ `embed-error`(接口抛错)/ `embed-dimension`(接口成功了,但库里没有一条维度相符的活跃向量)/ `no-embedder`(压根没装配 embedder——daemon `--embed off`、lightMode、宿主未装配;**这是预期态,不是故障**)。六值的唯一真源是 `src/api-standalone.js` 导出的 `CONTEXT_DEGRADE_REASONS`,契约测试从它派生断言集合。
|
|
560
|
+
|
|
561
|
+
`embed-dimension` 值得单说:它是「拿到了向量却不可比」——`searchVector` 对维度不符的行直接跳过,不抛错、只是命中为零,于是召回静默退回关键词/规则档,而嵌入侧一切正常。修法是重建索引,不是查端点,所以它必须和前四个分开。`no-embedder` 与前四个也分开:前者(`embed-unavailable`)是适配器坏了、要修代码,后者是语义路没开、要把嵌装扮上或开起来,主体与动作都不同;并成一个值会让按 `embed-unavailable` 告警的面板对每个轻量模式安装报警。用词则与宿主面 `/api/dsh-mneme/semantic` 的 `reason` 对齐——同一个物理条件在两个面叫同一个名字(那边是另一套枚举、另一个消费者,别顺手统一)。
|
|
562
|
+
|
|
563
|
+
**`unknown` 之下的判档故障另有一条 warn:** `unknown` 有三个生产者(空 `q` / 维度探针没跑成 / 有行却一条向量都读不出来),响应体只有 `tier` 一个键、分不出它们,而调用方对 `unknown` 的策略常常是「不告警」——所以判不出来的情形会另打一行 `dsh-mneme: /context tier left unknown: <what>`(探针抛错 / 没有可读的向量可比)。**它不是上面那六值的一员**(`tier` 不是 `degraded`,没有降级原因可记),运维程序不要按原因枚举解析它。
|
|
564
|
+
|
|
565
|
+
**预算:** 判档不是免费的。`q` 非空且装配了 embedder 时,一次 `/context` 真的会起一次查询嵌入(一次网络往返 + `embedTimeoutMs` 的超时窗口)——档位要回答的正是「这次嵌入成不成」。这里没有复用 `/health` 的数据源:standalone 的 `/health` 只回 `{ok:true}`,而 `embeddingDimensionBreakdown` 是宿主面的一次全表扫(每行 JSON.parse 一个向量),按请求跑会把语义路成本翻倍。维度探针因此走 `service.vectorDimensionState()`——**按匹配短路的存在性判据**(有没有一条活跃向量的维度等于查询维度),不是「读一行的维度」:混合维度库里 `LIMIT 1` 会抽错维(明明有可比的向量却报 `degraded`),首条恰好解析失败时又会漏报成 `full`。成本是一次无 `LIMIT` 的全 id 扫描 + 按匹配短路的解析:全库同维度(常态)时循环第一行就返回,只解析 1 个向量;只有一条都不匹配时才走完整个循环,而那正是要报 `degraded` 的情形,值得。解析复用 `getParsedEmbedding` 的缓存,缓存热不热取决于 `injectCandidates` 是否真的走了 vectorIndex 分支。
|
|
538
566
|
|
|
539
567
|
### curl 示例
|
|
540
568
|
|
|
@@ -579,9 +607,16 @@ dsh-mneme add --type decision --title "采用 SQLite" \
|
|
|
579
607
|
dsh-mneme get 42 # 查看单条
|
|
580
608
|
dsh-mneme delete 42 # 删除
|
|
581
609
|
dsh-mneme config show # 查看当前配置(token 打码)
|
|
610
|
+
dsh-mneme share --dry-run # 分享包:只看会进几条、跳过几条,不落盘
|
|
611
|
+
dsh-mneme share --out pack.json # 写一个可外发的脱敏包(3 MiB 水位在源头拒)
|
|
612
|
+
dsh-mneme receive pack.json # 吃包:默认干跑,一个字都不写
|
|
613
|
+
dsh-mneme receive pack.json --apply # 确认后真的吃进自己的库(整包一个事务,可重跑)
|
|
614
|
+
dsh-mneme undo-share pack.json --apply # 按 pack_id 整包收回(物删,不是归档)
|
|
582
615
|
```
|
|
583
616
|
|
|
584
617
|
> 所有读取/写入命令支持 `--json` 输出原始 JSON;`config path` 打印配置文件路径(`~/.dsh-mneme/cli.json`)。
|
|
618
|
+
> 人类可读档只转达服务端文案里的 `hint` / `bytes` / `limitBytes`;闸门的 `detail`(坏在哪一格)与
|
|
619
|
+
> 413 的 `limit` 要 `--json` 才看得见——排查收包失败时先加它。
|
|
585
620
|
|
|
586
621
|
### 独立服务(daemon,`dsh-mneme-serve`)
|
|
587
622
|
|
|
@@ -595,7 +630,7 @@ dsh-mneme-serve --embed off # 纯关键词 + BM
|
|
|
595
630
|
|
|
596
631
|
- **鉴权与端口**:Bearer token 与 DSH 面板 / CLI 共用同一份(kv `external_api`,首次启动自动生成并持久化到 `memory.db`);端口/主机解析链与「外部访问」一致(显式参数 > 持久值 > 默认 8790/127.0.0.1)。配置端口被占会**直接报错退出**(不做端口顺延)——第三方把 URL 写死,静默换端口等于坏。因此 **daemon 与 DSH 的「外部访问」二选一**,不要同端口同开。
|
|
597
632
|
- **安全**:daemon 使用明文 HTTP,不提供原生 TLS。指定非回环 `--host` 时,请勿直接把服务暴露给不可信网络;远程访问请走 TLS 终止代理或 SSH 隧道。
|
|
598
|
-
- **能力边界(第一期,无 LLM)**:存储 / 检索(关键词 + BM25 + 向量)/ 镜像同步与人改合并 / `POST /maintenance/reclaim` / `/bootstrap` 全可用;巩固(autoDream)与蒸馏不在 daemon 内——巩固只属于 DSH 宿主进程,这是与宿主「单写者」的机械保证。`--embed` 默认 `local`(自管 runtime 与嵌入模型缺失时自动取件,约 200MB;失败降级关键词并打日志),也可选 `ollama` / `openai`(读宿主面板的 vector-config)/ `off`。
|
|
633
|
+
- **能力边界(第一期,无 LLM)**:存储 / 检索(关键词 + BM25 + 向量)/ 镜像同步与人改合并 / `POST /maintenance/reclaim` / `POST /receive`(吃包入库与整包退出:结构落库 + 随提交排空嵌入,**不建实体**——外来行本来就不做抽取,见 [docs/SHARE.md](docs/SHARE.md))/ `/bootstrap` 全可用;巩固(autoDream)与蒸馏不在 daemon 内——巩固只属于 DSH 宿主进程,这是与宿主「单写者」的机械保证。`--embed` 默认 `local`(自管 runtime 与嵌入模型缺失时自动取件,约 200MB;失败降级关键词并打日志),也可选 `ollama` / `openai`(读宿主面板的 vector-config)/ `off`。
|
|
599
634
|
- **检索回执**:daemon 的 `/search` 同样落 `recall_runs`,第三方检索的复用统计不缺数。
|
|
600
635
|
- **生命周期**:stdout 仅就绪时打一行 `dsh-mneme-serve listening on http://host:port (pid N)`(供脚本解析实际端口),日志走 stderr;SIGINT/SIGTERM 优雅收库,Windows 强杀由 WAL 回放兜底。
|
|
601
636
|
- **已知限制**:与 DSH 同时运行属设计内场景(WAL 多进程并发),但去重是先查后写、库层无 UNIQUE 约束,双进程并发写同一 `(type, title, scope)` 有极小概率产生重复;daemon 与插件请同版本升级。细节与坑清单见 [docs/DAEMON.md](docs/DAEMON.md)。
|
|
@@ -645,7 +680,7 @@ Claude Code 挂载示例(项目根 `.mcp.json`;token 在面板「设置 →
|
|
|
645
680
|
│ 服务层:saveWithDedupe / injectCandidates │
|
|
646
681
|
│ / mergeHumanEdits / onWrite 钩子 │
|
|
647
682
|
├─────────────────────────────────────────────────┤
|
|
648
|
-
│ 模型接口:
|
|
683
|
+
│ 模型接口:11 个工具 + 自动注入 + 会话摘要 │
|
|
649
684
|
├─────────────────────────────────────────────────┤
|
|
650
685
|
│ autoDream:阈值调度 → LLM 决策清单 │
|
|
651
686
|
│ → 校验(fail-safe)→ 应用 → 摘要 │
|
|
@@ -663,7 +698,10 @@ src/
|
|
|
663
698
|
├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子、scope 加权/硬过滤、冲突队列)
|
|
664
699
|
├── scope.js # 作用域解析(agentPreset + workspace registry 反查,issue #17)
|
|
665
700
|
├── config.js # schemastery 配置 schema
|
|
666
|
-
├── tools.js #
|
|
701
|
+
├── tools.js # 11 个模型工具(defineTool;memory_organize 要 organizeEnabled 打开才注册,开了是 12 个)
|
|
702
|
+
├── user-settings.js # 「用户设置」注入块(人格 → 画像 → 规则)的唯一渲染处,注入与面板预览共用
|
|
703
|
+
├── profile-extract.js # 从活跃记忆提取「助手眼中的用户」画像草稿(只出草稿,不写 user_settings)
|
|
704
|
+
├── organize.js # agent 主动整理:dryRun 比对报告 → agent 判断 → apply 落库(筛除=归档不删,留 dream_runs 回执)
|
|
667
705
|
├── inject.js # systemPrompt.context 动态注入(含花括号转义)
|
|
668
706
|
├── summarize.js # 会话结束 LLM 摘要
|
|
669
707
|
├── dream.js # autoDream 调度 + runDream(LLM 决策 + 摘要)
|
|
@@ -679,7 +717,7 @@ src/
|
|
|
679
717
|
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
|
|
680
718
|
└── index.js # 插件接线
|
|
681
719
|
lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
|
|
682
|
-
test/ #
|
|
720
|
+
test/ # 2541 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
683
721
|
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 运行时清单
|
|
684
722
|
```
|
|
685
723
|
|
|
@@ -688,7 +726,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
688
726
|
```bash
|
|
689
727
|
cd dsh-mneme
|
|
690
728
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
691
|
-
npm test # 运行
|
|
729
|
+
npm test # 运行 2541 个测试
|
|
692
730
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
693
731
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
694
732
|
```
|
|
@@ -710,6 +748,7 @@ npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动
|
|
|
710
748
|
- [连续性抢救(#249 N3):触发器口径与事件契约](docs/CONTINUITY.md)
|
|
711
749
|
- [Sleep Mode:空闲四阶段深度维护](docs/SLEEP.md)
|
|
712
750
|
- [存储与回收:表结构、保留期、`dsh-mneme reclaim`](docs/STORAGE.md)
|
|
751
|
+
- [记忆分享包:出包与收包的口径、信任边界与实测(`dsh-mneme share` / `receive` / `undo-share`)](docs/SHARE.md)
|
|
713
752
|
- [独立服务 daemon(`dsh-mneme-serve`)](docs/DAEMON.md)
|
|
714
753
|
- [与 Zep / Graphiti / Mem0 / Letta / Hindsight 的能力对比](../docs/COMPARISON.md)
|
|
715
754
|
|
|
@@ -738,3 +777,7 @@ dsh-mneme 的机制设计与下列公开研究相互印证(以下引用均逐
|
|
|
738
777
|
## 📜 License
|
|
739
778
|
|
|
740
779
|
MIT
|
|
780
|
+
|
|
781
|
+
## 🤝 参与之前
|
|
782
|
+
|
|
783
|
+
行为守则:[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)。它管三件事——**署名归人**(模型不进作者行)、**责任落在签下作者行的人身上**、**发言权按证据分配,不按身份**。参与 issue / PR / 评审即受其约束;使用本插件不受约束,MIT 已经授权。
|
package/bin/cli.mjs
CHANGED
|
@@ -77,6 +77,18 @@ const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外
|
|
|
77
77
|
--vacuum 执行后整库 VACUUM(需配 --apply)
|
|
78
78
|
只清「可由记忆库重建的输入快照」与「归档行的向量」,
|
|
79
79
|
一条记忆都不删;收益按 VACUUM 前后体积量
|
|
80
|
+
share [--alias 名字] [--type a,b] [--tag 标签] --out <文件>
|
|
81
|
+
打一个可外发的脱敏分享包(不改库,纯读)
|
|
82
|
+
--alias 名字 在包里署名,对方看到的是这个而不是你的路径
|
|
83
|
+
--type/--tag 收窄到某几类/某个标签(超限分批用这两个)
|
|
84
|
+
--out <文件> 写出包文件;--dry-run 只报数不落盘
|
|
85
|
+
跳过归档/已遗忘/只读指针行/summary,密钥 PII 整条不进包
|
|
86
|
+
receive <包文件> [--apply]
|
|
87
|
+
把对方 share 出来的包吃进自己的库(默认干跑,一个字都不写)
|
|
88
|
+
外来行标 source=imported,不参与本机的巩固/降级,也不并入你的条目
|
|
89
|
+
--apply 才真的写;重复吃同一个包不会多出第二份
|
|
90
|
+
undo-share <包文件> [--apply]
|
|
91
|
+
按 pack_id 收起整包(物删:包还能重新导入,归档位会占住 UNIQUE)
|
|
80
92
|
|
|
81
93
|
选项:
|
|
82
94
|
--url <url> 覆盖服务地址
|
|
@@ -98,7 +110,12 @@ const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外
|
|
|
98
110
|
${CLI_NAME} get 42
|
|
99
111
|
${CLI_NAME} delete 42
|
|
100
112
|
${CLI_NAME} reclaim (先看:会清几行、库多大)
|
|
101
|
-
${CLI_NAME} reclaim --apply --vacuum
|
|
113
|
+
${CLI_NAME} reclaim --apply --vacuum (确认后执行,并把空间真的收回来)
|
|
114
|
+
${CLI_NAME} share --dry-run (先看:会进几条、为什么跳过那些,不落盘)
|
|
115
|
+
${CLI_NAME} share --out pack.json (写包;~/… 由 CLI 自己展开,PowerShell/cmd 不会)
|
|
116
|
+
${CLI_NAME} receive pack.json (吃包:默认干跑,只报账,一个字都不写)
|
|
117
|
+
${CLI_NAME} receive pack.json --apply (确认数字后真的吃进自己的库;重跑同一个包 created=0)
|
|
118
|
+
${CLI_NAME} undo-share pack.json --apply (按包里那个 pack_id 整包收回:物删,之后还能重新导入)`;
|
|
102
119
|
|
|
103
120
|
/* ---------------------------------- 参数解析 ---------------------------------- */
|
|
104
121
|
|
|
@@ -124,8 +141,10 @@ const KNOWN_FLAGS = new Set([
|
|
|
124
141
|
'url', 'token', 'type', 'title', 'content', 'importance', 'tags', 'source',
|
|
125
142
|
'limit', 'offset', 'mode', 'topk', 'top-k', 'min-importance', 'minimportance',
|
|
126
143
|
'older-than', 'olderthandays',
|
|
144
|
+
'alias', 'tag', 'out',
|
|
127
145
|
// 布尔旗标
|
|
128
|
-
'apply', 'vacuum', 'json', 'help', 'version'
|
|
146
|
+
'apply', 'vacuum', 'json', 'help', 'version',
|
|
147
|
+
'dry-run', 'dryrun',
|
|
129
148
|
]);
|
|
130
149
|
|
|
131
150
|
/** `--foo=bar` / `--foo` 取出小写键名;非旗标词返回 null。 */
|
|
@@ -246,6 +265,24 @@ function failHttp(cfg, status, pathname, data, text) {
|
|
|
246
265
|
} else if (status === 404) {
|
|
247
266
|
hint = `\n资源不存在:${pathname}`;
|
|
248
267
|
}
|
|
268
|
+
// 拒答体自己带的下一步必须转出去,不然命令侧只能再写一份错误分支。撞上的这一刀是
|
|
269
|
+
// share 的 413:响应体是 {error:"too-large", bytes, limitBytes, hint, report},
|
|
270
|
+
// 旧实现只取 data.error,于是现场只剩「请求失败(HTTP 413):too-large」——「大了
|
|
271
|
+
// 多少」与「用 --type/--tag 分批」那两句留在体里没人看得见(措辞只许有一份,就在
|
|
272
|
+
// 拒发的那侧,同 RECLAIM_CONFIRM_HINT 的口径)。完整 report(含 skip_list)走
|
|
273
|
+
// --json:下面 rawJson 把整个响应体原样交给 stderr,这里不复制第二份打印器。
|
|
274
|
+
//
|
|
275
|
+
// 已知取舍(C10 记档,见 docs/SHARE.md「已知还没做」):这里只转 hint / bytes /
|
|
276
|
+
// limitBytes 三样,所以 receive 那侧 400 的 `detail`(闸门指到哪一格)与数据面 413 的
|
|
277
|
+
// `limit` 到不了人类可读的输出。刻意不补:补一格就得在这里多一条按路由分支的判断,
|
|
278
|
+
// 而「错误文案只许有一份」是 #411 那条纪律的另一半。要逐格看就加 --json——rawJson
|
|
279
|
+
// 那行给的正是原样响应体,一格都不丢。
|
|
280
|
+
if (data && typeof data === 'object') {
|
|
281
|
+
if (typeof data.hint === 'string' && data.hint.trim()) hint += `\n${data.hint.trim()}`;
|
|
282
|
+
if (Number.isFinite(data.bytes) && Number.isFinite(data.limitBytes)) {
|
|
283
|
+
hint += `\n量到 ${formatBytes(data.bytes)},水位 ${formatBytes(data.limitBytes)}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
249
286
|
fail(cfg, `请求失败(HTTP ${status})${serverError ? `:${serverError}` : ''} [${pathname}]${hint}`, {
|
|
250
287
|
rawJson: cfg.jsonMode && text ? text : undefined,
|
|
251
288
|
});
|
|
@@ -687,6 +724,176 @@ async function cmdReclaim(cfg, flags) {
|
|
|
687
724
|
}
|
|
688
725
|
}
|
|
689
726
|
|
|
727
|
+
// --out 的例子写的是 `~/Desktop/pack.json`。Git Bash 会替她展开,PowerShell 与 cmd
|
|
728
|
+
// 不会——那两侧 `~/…` 是字面量,writeFileSync 直接 ENOENT,现场只剩一坨堆栈。CLI 不
|
|
729
|
+
// 接管 shell 的活,但引号里这一格得自己补(同 CONFIG_DIR 走 os.homedir())。
|
|
730
|
+
function expandHome(p) {
|
|
731
|
+
if (typeof p !== 'string') return p;
|
|
732
|
+
if (p !== '~' && !p.startsWith('~/') && !p.startsWith('~\\')) return p;
|
|
733
|
+
const rest = p.slice(1).replace(/^[/\\]+/, '');
|
|
734
|
+
return path.join(os.homedir(), rest);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function printShare(pack, { dryRun }) {
|
|
738
|
+
const r = pack?.report ?? {};
|
|
739
|
+
const s = r.skipped ?? {};
|
|
740
|
+
console.log(`${dryRun ? '干跑报告' : '分享包'}: ${r.included ?? 0} 条` +
|
|
741
|
+
(pack?.alias ? ` 来自 ${pack.alias}` : ''));
|
|
742
|
+
const named = [['archived', '归档'], ['forgotten', '已遗忘'], ['readonly_type', '只读指针行'],
|
|
743
|
+
['excluded_type', '不随包走'], ['blank_or_noise', '空白/噪声'], ['sensitive', '密钥/PII']];
|
|
744
|
+
const parts = named.filter(([k]) => (s[k] ?? 0) > 0).map(([k, zh]) => `${zh} ${s[k]}`);
|
|
745
|
+
console.log(` 跳过: ${parts.length ? parts.join(', ') : '无'}`);
|
|
746
|
+
// 折叠不是跳过:那条记忆够格进包,只是包里已经有了它(同标题同正文同内容锚、scope
|
|
747
|
+
// 不同的两条)。不印出来的话 `included` 会比她心里的「够格的条数」少几条,而那正好是
|
|
748
|
+
// 会让人怀疑口径少发了的那类差额。src/share.js 的 report.deduped。
|
|
749
|
+
if ((r.deduped ?? 0) > 0) console.log(` 包内重复已折叠: ${r.deduped} 条(对面只吃一条)`);
|
|
750
|
+
for (const item of (r.skip_list ?? []).slice(0, 20)) {
|
|
751
|
+
// 标题为空有两种:这条本来没标题,或它整行被密钥判据命中、标题在源头就留空了
|
|
752
|
+
// (src/share.js:skip_list 随包外发,而凭据常常就写在标题里)。终端要让人看清
|
|
753
|
+
// 后一种是「有意不给」,不是「数据缺失」。
|
|
754
|
+
console.log(` - [${item.reason}] ${item.title || '(标题不外发)'}`);
|
|
755
|
+
}
|
|
756
|
+
// 判据用「六个计数之和 > 清单长度」而不是「清单等于 50」:前者是「账比清单长」这个事实本身,
|
|
757
|
+
// 恰好 50 条被跳时后者会误报截断(面板侧同一条判据)。50 这个数不在这里出现,
|
|
758
|
+
// 水位改了判据仍然对。
|
|
759
|
+
const skippedTotal = ['archived', 'forgotten', 'readonly_type', 'excluded_type', 'blank_or_noise', 'sensitive']
|
|
760
|
+
.reduce((n, k) => n + (r.skipped?.[k] ?? 0), 0);
|
|
761
|
+
if (skippedTotal > (r.skip_list ?? []).length) console.log(' … 跳过清单已截断,计数仍为全量');
|
|
762
|
+
if (dryRun) console.log(` 没写文件:加 --out <路径> 才会落盘`);
|
|
763
|
+
else console.log(` 发给对方即可,她用自己的 ${CLI_NAME} receive 吃进去`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function cmdShare(cfg, flags) {
|
|
767
|
+
requireToken(cfg);
|
|
768
|
+
const out = expandHome(flagValue(flags, 'out'));
|
|
769
|
+
const dryRun = flagBool(flags, 'dry-run') || flagBool(flags, 'dryrun');
|
|
770
|
+
if (!out && !dryRun) {
|
|
771
|
+
fail(
|
|
772
|
+
cfg,
|
|
773
|
+
`share 要么写文件(--out <路径>),要么只看报告(--dry-run):` +
|
|
774
|
+
`不写文件又把整库打到 stdout 会让报告被 JSON 淹掉。\n` +
|
|
775
|
+
`例子:${CLI_NAME} share --out ~/Desktop/pack.json`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
const typeRaw = flagValue(flags, 'type');
|
|
779
|
+
const body = {};
|
|
780
|
+
if (typeRaw) body.type = typeRaw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
781
|
+
const alias = flagValue(flags, 'alias');
|
|
782
|
+
if (alias) body.alias = alias;
|
|
783
|
+
const tag = flagValue(flags, 'tag');
|
|
784
|
+
if (tag) body.tag = tag;
|
|
785
|
+
// 超水位那次服务端答 413,apiRequest 对非 2xx 走的是 failHttp:它把响应体里的 hint
|
|
786
|
+
// 与量到的字节转出去再 exit(1)。所以这里**没有**可达的 error 分支——加 `if
|
|
787
|
+
// (pack?.error)` 就是第二份出口,措辞迟早跟拒发那一侧分家(#411 的同一口径)。
|
|
788
|
+
const pack = await apiRequest(cfg, 'POST', '/share', { body });
|
|
789
|
+
const r = pack?.report ?? {};
|
|
790
|
+
if (cfg.jsonMode) {
|
|
791
|
+
console.log(stringify(dryRun ? { report: r, pack } : { report: r, file: out }));
|
|
792
|
+
} else {
|
|
793
|
+
printShare(pack, { dryRun });
|
|
794
|
+
}
|
|
795
|
+
if (dryRun) return;
|
|
796
|
+
fs.writeFileSync(out, JSON.stringify(pack), 'utf8');
|
|
797
|
+
// --json 时 stdout 只许那一份 JSON:后面追一行「已写出」会把 JSON.parse 撞在
|
|
798
|
+
// position 536(jq 同理)。落盘路径已在上面的 payload 里(file 键),这行只对
|
|
799
|
+
// 人有用,就只给人看(同 cmdReclaim:json 模式打印完就结束)。
|
|
800
|
+
if (!cfg.jsonMode) console.log(` 已写出: ${out}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 入库面(车道 C)。包文件在**本地**读、把包内容整个送上去:服务端永远看不到路径,
|
|
804
|
+
// 也就不需要任何文件系统权限——这是 receive 协议选「传包不传路径」的全部理由。
|
|
805
|
+
// confirm 的脾气照 reclaim:缺省即干跑,写收件人的库(或物删整包)要 --apply 显式确认。
|
|
806
|
+
async function cmdReceive(cfg, rest, flags) {
|
|
807
|
+
requireToken(cfg);
|
|
808
|
+
const file = rest[0];
|
|
809
|
+
if (!file) {
|
|
810
|
+
fail(cfg, `receive 需要包文件:${CLI_NAME} receive <包文件>(不带 --apply 只干跑,一个字都不写)`);
|
|
811
|
+
}
|
|
812
|
+
let pack;
|
|
813
|
+
try {
|
|
814
|
+
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
815
|
+
} catch (e) {
|
|
816
|
+
// 读坏/读不到都在发请求之前拒掉:半读出来的包发过去只会换回一个闸门错,
|
|
817
|
+
// 而那一格现场已经说不清是「文件本来就这样」还是「传输坏了」。
|
|
818
|
+
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n包文件是对方 ${CLI_NAME} share --out 出来的那个 .json。`);
|
|
819
|
+
}
|
|
820
|
+
const applied = flagBool(flags, 'apply');
|
|
821
|
+
const body = { pack };
|
|
822
|
+
if (applied) body.confirm = true;
|
|
823
|
+
const data = await apiRequest(cfg, 'POST', '/receive', { body });
|
|
824
|
+
if (cfg.jsonMode) {
|
|
825
|
+
console.log(stringify(data));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
printReceive(data, { applied });
|
|
829
|
+
if (!applied) console.log(' 没写库:确认数字之后加 --apply 真的导入');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 撤销收的是**包文件**而不是裸 pack_id:让人抄 16 位十六进制去删自己库里的行,
|
|
833
|
+
// 抄错一位就是「删了个没删成的包还自以为收掉了」。pack_id 就写在包文件第一层。
|
|
834
|
+
async function cmdUndoShare(cfg, rest, flags) {
|
|
835
|
+
requireToken(cfg);
|
|
836
|
+
const file = rest[0];
|
|
837
|
+
if (!file) {
|
|
838
|
+
fail(cfg, `undo-share 需要当初那个包文件(pack_id 从文件里取):${CLI_NAME} undo-share <包文件> [--apply]`);
|
|
839
|
+
}
|
|
840
|
+
let pack;
|
|
841
|
+
try {
|
|
842
|
+
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
843
|
+
} catch (e) {
|
|
844
|
+
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n要的是当初 ${CLI_NAME} share --out 出来、发给对面那个 .json。`);
|
|
845
|
+
}
|
|
846
|
+
// trim 在这儿做:只有空格的 pack_id 与「没有 pack_id」是同一种「这个文件收不了」,
|
|
847
|
+
// 放它出门只是绕一圈换回 400 missing-pack-id,而那一格会被读成服务端的问题(服务端
|
|
848
|
+
// parseUndoBody 同样 trim,这里不改它的行为,只是不必为空格 round-trip)。
|
|
849
|
+
const packId = typeof pack?.pack_id === 'string' ? pack.pack_id.trim() : '';
|
|
850
|
+
if (!packId) {
|
|
851
|
+
fail(cfg, `这个文件里没有 pack_id,收起不了:${file}\nundo-share 收的是整包,编号就写在包文件的第一层,不用也不该手抄 16 位十六进制。`);
|
|
852
|
+
}
|
|
853
|
+
const applied = flagBool(flags, 'apply');
|
|
854
|
+
const body = { packId };
|
|
855
|
+
if (applied) body.confirm = true;
|
|
856
|
+
const data = await apiRequest(cfg, 'POST', '/receive/undo', { body });
|
|
857
|
+
if (cfg.jsonMode) {
|
|
858
|
+
console.log(stringify(data));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
printReceive({ ...data, created: data?.deleted, alias: '' }, { applied, undo: true });
|
|
862
|
+
if (!applied) console.log(' 没删:确认条数之后加 --apply 真的收起这个包');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function printReceive(d, { applied, undo = false }) {
|
|
866
|
+
const conflicts = Array.isArray(d?.title_conflicts) ? d.title_conflicts : [];
|
|
867
|
+
console.log(`${undo ? (applied ? '已收起' : '将收起') : applied ? '已导入' : '干跑'}: ${d?.created ?? 0} 条` +
|
|
868
|
+
(d?.alias ? ` 来自 ${d.alias}` : '') + (d?.skipped_existing ? ` 已存在 ${d.skipped_existing}` : ''));
|
|
869
|
+
if (undo && applied) {
|
|
870
|
+
// 服务端是物删不是归档(src/share-import.js 的 deleteSharePack):这句话是 --apply
|
|
871
|
+
// 与人之间唯一的说明书,不许软化成「移除」。干跑那一笔一个字都没动,说「直接从库里
|
|
872
|
+
// 带走」是假话,还会把下面的样例压成「已经删掉的这几条」——干跑那侧的话术是 cmdUndo-
|
|
873
|
+
// Share 结尾的「没删:…加 --apply」,两句不许同时出现(同一件事说两遍就没人细看)。
|
|
874
|
+
console.log(' 物删:这些行直接从库里带走,不是归档;包文件还在,重新 receive 一遍即可再来');
|
|
875
|
+
}
|
|
876
|
+
if (conflicts.length) {
|
|
877
|
+
// 撞名是 advisory 读数(src/share-import.js:只对本轮真正会新建的条目判),
|
|
878
|
+
// 所以这里只说「这几条要你看一眼」,永远不许说「除此之外没有别的撞名」。
|
|
879
|
+
console.log(` 撞名提醒:和你自己的条目同名 ${conflicts.length} 条(同类型同标题,不会被并入,需要你处置):`);
|
|
880
|
+
for (const c of conflicts.slice(0, 10)) console.log(` - [${c.type}] ${c.title}`);
|
|
881
|
+
if (conflicts.length > 10) console.log(' … 只列前 10 条');
|
|
882
|
+
}
|
|
883
|
+
const sample = Array.isArray(d?.sample) ? d.sample : [];
|
|
884
|
+
if (sample.length) {
|
|
885
|
+
// 撞名那段有标题、有截断话,样例这段原先两样都没有:服务端把 sample 截到 20
|
|
886
|
+
// (src/share-import.js 的 DELETE_SAMPLE_MAX),CLI 再截到 10,而 deleted 才是全集。
|
|
887
|
+
// 一个 500 行的包就这样留下 10 条无名要点,人会当成「要收走的就是这些」。
|
|
888
|
+
// 全集那一格只报服务端的数,不报「还剩几条」——两次截断混在一起就没人能对得上账。
|
|
889
|
+
console.log(' 样例:下面只是这一批里的几条,不是全部清单');
|
|
890
|
+
const shown = sample.slice(0, 10);
|
|
891
|
+
for (const s of shown) console.log(` · [${s.type}] ${s.title}`);
|
|
892
|
+
const total = typeof d?.deleted === 'number' ? d.deleted : (d?.created ?? 0);
|
|
893
|
+
if (shown.length < total) console.log(` … 只列前 ${shown.length} 条;整批共 ${total} 条(这个数来自服务端计数)`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
690
897
|
/* ---------------------------------- 入口 ---------------------------------- */
|
|
691
898
|
async function main(argv) {
|
|
692
899
|
const { positionals, flags } = parseArgv(argv);
|
|
@@ -724,6 +931,12 @@ async function main(argv) {
|
|
|
724
931
|
return cmdDelete(cfg, rest);
|
|
725
932
|
case 'reclaim':
|
|
726
933
|
return cmdReclaim(cfg, flags);
|
|
934
|
+
case 'share':
|
|
935
|
+
return cmdShare(cfg, flags);
|
|
936
|
+
case 'receive':
|
|
937
|
+
return cmdReceive(cfg, rest, flags);
|
|
938
|
+
case 'undo-share':
|
|
939
|
+
return cmdUndoShare(cfg, rest, flags);
|
|
727
940
|
default:
|
|
728
941
|
fail(cfg, `未知命令: ${cmd}\n运行 \`${CLI_NAME} --help\` 查看帮助。`);
|
|
729
942
|
}
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -117,11 +117,13 @@
|
|
|
117
117
|
|
|
118
118
|
| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|
|
119
119
|
|---|---|---|---|
|
|
120
|
-
| `distillRecords.enabled` **light** | `false` | 蒸馏时顺带留存窗口原文 +
|
|
120
|
+
| `distillRecords.enabled` **light** | `false` | 蒸馏时顺带留存窗口原文 + 写记忆级溯源链接;同时是溯源读出口(`GET /memory/provenance` 与宿主内 `/api/dsh-mneme/memory/provenance`)的总闸 | opt-in;关 = 一行不写、连收集都不做,行为与 #399 之前逐字节一致;读出口此时回 `409 {"error":"distill-records-disabled"}`(与「装配没接线」的 503 分开读数)。**关开关 ≠ 删掉已留存的原文**:只不再经 HTTP 提供,盘上的行要 `dsh-mneme reclaim` 或重新开启后的启动裁剪才走。开关是**启动快照**(与 `panel_mode` 同节奏),面板改完要到下次启动生效,面板会显示「待重启」 |
|
|
121
121
|
| `distillRecords.retentionDays` | `90` | 原文保留天数(1–3650) | **只管启动期裁剪**(`src/index.js`,`enabled` 时动手)。`dsh-mneme reclaim` 不读这个键:它对三个表用同一条 `--older-than` 截止(默认 7 天),想对齐就显式传成与 `retentionDays` 相同的值(见 STORAGE.md)。两处都**只删 `distill_records`**,溯源链接保留指向空区间(显示「来源已过期」),绝不级联删记忆 |
|
|
122
122
|
|
|
123
123
|
两个键都走 feature_flags 白名单(面板可启停);`enabled` 进 lightMode 预设关闭清单,`retentionDays` 不进(它是时长不是行为面,用户显式设过就照旧)。`dsh-mneme reclaim` 的报告从两项变三项(`planned` / `column_bytes` / `cleared` 各多一个 `distill_records`),入口语义(默认 dry-run、`--apply` 才动手)不变。已知边界:无历史回填、从未蒸馏过的窗口不留记录、不覆盖工具结果与 code 输出。
|
|
124
124
|
|
|
125
|
+
`enabled` 这一格**同时管读写两侧**(#509 评审整改后):写侧是蒸馏时是否落盘(`src/summarize.js:988`),读侧是溯源读出口是否把原文读出去(判据在 `src/provenance-http.js`,与写侧逐字同源,只此一份)。关着时读出口回 `409 {"error":"distill-records-disabled"}`——但**盘上的原文不会因此消失**:保留期裁剪与写入同在 `src/index.js:270` 一道门里,关掉开关意味着「不再写、启动期也不再裁」,已留的行会一直躺在库里,直到 `dsh-mneme reclaim`(它不读这个键,手动可删)或重新开启后的启动裁剪。所以「关掉就干净了」是误读,诚实的表述是「关掉 = 不再经 HTTP 提供」。
|
|
126
|
+
|
|
125
127
|
## 注入
|
|
126
128
|
|
|
127
129
|
| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|
|
@@ -170,7 +172,7 @@
|
|
|
170
172
|
| `dreamMinExplicitCoverage` | `0.5` | 显式决策覆盖率下限(0–1) | 防截断输出被隐式 keep 洗白 |
|
|
171
173
|
| `dreamSkipInvalid` | `true` | 跳过单条非法决策、应用合法子集、run 记 degraded(#89) | `false` = 恢复整单拒绝 |
|
|
172
174
|
| `allowCrossTypeMerge` | `false` | 放宽跨类型合并检查 | opt-in;`dreamSkipInvalid` 关时跨类型 merge 直接整单拒绝 |
|
|
173
|
-
| `dreamMergeGuard` | `true` | guarded 类型 merge 护栏(#339/E8):合并对象命中长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的 merge
|
|
175
|
+
| `dreamMergeGuard` | `true` | guarded 类型 merge 护栏(#339/E8):合并对象命中长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的 merge 决策跳过——E8 实测巩固损耗里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉。**#521 起带逃生门**:所有 id 同类型且 `reason` 命中「重复/过时」词表(与 archive 护栏同一份 `ARCHIVE_RATIONALE_RE`)时放行——archive 早就有这道门,merge 没有,于是一份真重复的 preference 永远合不掉而提示词还在催模型合并它 | 默认开(同类压缩管线实测:安全规则被同速压缩后一轮只剩 53%、五轮只剩 10%,全程无报错);关闭 = guarded 类型的 merge 不再被跳过,「更精炼的摘要」重新成为约束失真的主通道。`dreamSkipInvalid`(默认开)时被跳条目进 `dream_runs.skipped`、run 记 degraded,关闭时整单拒绝 |
|
|
174
176
|
| `dreamNarrativeEnabled` **light** | `false` | dream 期间按共享 tag 聚类合成叙述条(#164 对齐) | opt-in;按需检索、不常驻注入 |
|
|
175
177
|
| `dreamNarrativeMinCluster` | `3` | 成簇门槛(共享同一 tag 的记忆数,2–20) | — |
|
|
176
178
|
| `documentMemoryEnabled` **light** | `false` | document 型记忆:长文档指针行,全文归 agent(#230) | opt-in;注册校验 + C2 去重 + supersede 记账 |
|
package/docs/DAEMON.md
CHANGED
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
daemon 是**数据面**,不是第二个宿主:
|
|
8
8
|
|
|
9
|
-
- **有**:存储(SQLite)、检索(关键词 + BM25 + 向量,`/search` 统一召回,与宿主共用 `src/semantic.js` 同一套装配)、注入候选(`/context`,issue #370,与宿主注入管线同一份 `injectCandidates`)、镜像同步与人改合并、`/maintenance/reclaim`、`/bootstrap`、recall_runs 检索/注入回执。
|
|
9
|
+
- **有**:存储(SQLite)、检索(关键词 + BM25 + 向量,`/search` 统一召回,与宿主共用 `src/semantic.js` 同一套装配)、注入候选(`/context`,issue #370,与宿主注入管线同一份 `injectCandidates`)、镜像同步与人改合并、`/maintenance/reclaim`、`/share`(记忆分享包,纯读,见 [SHARE.md](SHARE.md))、`/receive` 与 `/receive/undo`(吃包入库与整包退出,车道 C;写的是**本机自己的库**,与 `/share` 同一条 4 MiB 入站上限)、`/bootstrap`、recall_runs 检索/注入回执。
|
|
10
10
|
- **没有(第一期,无 LLM)**:巩固(autoDream)、蒸馏(autoSummarize)、实体抽取、sleep、注入/工具/面板路由。前两者是**结构性缺失**而非开关——daemon 装配里没有 LLM 句柄,巩固只属于 DSH 宿主进程。这就是 daemon 与宿主「单写者」的机械保证(AGENTS.md externalApi/autoDream 单侧纪律的 daemon 版),不依赖用户自觉。
|
|
11
|
+
- **收包在 daemon 上付的是什么**:一个事务里逐条先查 `share_external_id` 再 `store.save`,嵌入随最外层 COMMIT 排空(`--embed local` 时真打向量,`off` 时只落结构),镜像重渲染照 daemon 自己那条回灌路走。**实体一个都不建**——不是 daemon 缺 LLM 才不建,车道 C 对外来行本来就不做抽取(有向量、无实体,实体与图检索看不见它们),宿主侧同样如此。成本的量级读数见 [SHARE.md](SHARE.md)「收侧」那一节实测。
|
|
11
12
|
|
|
12
13
|
与宿主装配(`src/index.js` apply)的关系:`src/serve.js` 只搬数据面那一半,每步注释锚定 index.js 来源行号;刻意不抽公共装配函数(apply 其余环节与宿主 ctx 纠缠,防御段纪律「最后动或不动」)。装配漂移风险由 `test/serve-bin.test.js` 的多进程共存用例兜底(两进程真开同一个库互写互读)。
|
|
13
14
|
|
|
14
15
|
## 2. 对外接口
|
|
15
16
|
|
|
16
|
-
路由面 = `src/api-standalone.js` 全表(health/status/profile/rules/memories
|
|
17
|
+
路由面 = `src/api-standalone.js` 全表(health/status/profile/rules/memories 读写/`/memory/provenance`(记忆级溯源读出口,#421)/search/context/share/receive(含 `/receive/undo`)/maintenance/bootstrap),鉴权同源(Bearer + timingSafeEqual,`GET /health` 免鉴权)。daemon 自身零新路由;#370 的 `/context` 落在 api-standalone 工厂里,宿主外部访问与 daemon 自动同享(参数、scope 语义与 recall_runs 注入分账见包 README 路由表)。daemon 侧唯一自有选项是 `strictPort`(见 §4)。
|
|
18
|
+
|
|
19
|
+
`/memory/provenance` 的开关门(#509 评审整改):这个面返回蒸馏窗口的**逐条会话原文**,所以它吃 `distillRecords.enabled`——与写侧 `src/summarize.js:988` 同一枚开关、同一个判据表达式(读侧唯一一份在 `src/provenance-http.js` 的 `isDistillRecordsEnabled`)。关着时回 `409 {"error":"distill-records-disabled"}`,不是 404(本端点刻意不就「这条记忆是否存在」作断言:未知 id 是 200 + 空 windows),也不是 503(那是「装配没接线」,`src/index.js:589-591` 立的可区分性原则)。状态码阶梯:`401` 无 token → `400` 缺 id → `409` 功能关了 → `503` 装配没接线 → `200`。daemon 读的是**面板持久化的开关**(`serve.js` 把 `feature_flags` 里的 `distillRecords.enabled` 展开进 cfg,与宿主 `index.js:217-233` 同款),与宿主共用同一个 memory.db、同一份 kv——所以用户在面板里开着,daemon 这一面就读得到。
|
|
20
|
+
|
|
21
|
+
残留不对称要知道:daemon 结构上看不到宿主的 **bundle config**(第一期不吃宿主 config schema),所以「只写在部署侧 bundle 配置里打开、从没在面板点过」的用户,宿主认、daemon 不认——daemon 会对每个请求回 409。这不是故障,是 daemon 装配面的已知边界;要让 daemon 也认,去面板里把开关显式打开一次(面板的「待重启」提示同样适用:开关是启动快照,改完要重启才生效)。
|
|
22
|
+
|
|
23
|
+
`/context` 的档位信号(#383):响应多一个 `tier`(`full` 语义档 / `degraded` 规则档 / `unknown` 语义路未启用或判不出来,fail-closed),**具体降级原因只在 stderr**——`dsh-mneme: /context degraded to the rule tier (<reason>)`,不进响应体。原因是给运维的读数,六值互不吞并(成例「修法不同就分开」):`embed-unavailable` 无可用接口(embedder 对象在、缺 `embedSingle`/`embed`,修适配器)/ `embed-empty` 空向量 / `embed-timeout` 挂起 / `embed-error` 抛错 / `embed-dimension` 维度不符(有向量但没有一条与查询同维,修法是重建索引)/ `no-embedder` 压根没装配 embedder(`--embed off` / lightMode / 宿主未装配,**预期态不是故障**,修法是把嵌装扮上或开起来)。`tier` 是给调用方的读数,两套消费者分开;六值的唯一真源是 `src/api-standalone.js` 的 `CONTEXT_DEGRADE_REASONS`,契约测试从它派生。`unknown` 之下(维度探针没跑成 / 有行却一条向量都读不出来)另有一行 `dsh-mneme: /context tier left unknown: <what>`,**不是**那六值的一员(`tier` 不是 degraded,没有降级原因可记),运维程序不要按原因枚举解析它——没有它,调用方对 unknown 的策略是「不告警」时,一次真实的判档故障会整个消失。`no-embedder` 不并进 `embed-unavailable` 是故意的:那两者共用「语义路没通」的结果,但一个是适配器坏了(异常)、一个是语义路没开(设计内常态),并成一个值会让按 `embed-unavailable` 告警的面板对每个轻量模式安装报警;用词则与宿主面 `/api/dsh-mneme/semantic` 的 `reason` 对齐(那边是另一套枚举、另一个消费者,别顺手统一)。这里最值得盯的是 `embed-dimension`:嵌入接口一切正常,但库里是旧维度向量,`searchVector` 直接跳过、不抛错,召回静默退回关键词档——修法是重建索引,不是查端点。判档有成本:`q` 非空且装了 embedder 时一次 `/context` 会真起一次查询嵌入(一次往返 + `embedTimeoutMs` 窗口),维度探针本身是**按匹配短路的存在性判据**(有没有一条活跃向量与查询同维,不是 `LIMIT 1` 读一行——混合维度库里那会抽错维误报 degraded、首条损坏又会漏报 full):一次无 `LIMIT` 的全 id 扫描 + 短路解析,同维度健康库只解析 1 个向量,只有要报 degraded 时才走完全表;不复用 `/health`(它只回 `{ok:true}`)也不做全表扫。
|
|
17
24
|
|
|
18
25
|
CLI:
|
|
19
26
|
|
package/docs/DISTILL.md
CHANGED
|
@@ -111,10 +111,15 @@ distillRecords:
|
|
|
111
111
|
**不读这个键**,跟同一条 `--older-than`(默认 7 天)走——两个窗口互不联动,想一次对齐就把
|
|
112
112
|
`--older-than` 显式传成与 `retentionDays` 相同的值。
|
|
113
113
|
|
|
114
|
-
删记忆是另一条路:`store.remove(id)`
|
|
115
|
-
`
|
|
116
|
-
`
|
|
117
|
-
|
|
114
|
+
删记忆是另一条路:`store.remove(id)` 在事务内**显式**清掉它的溯源链接。这条显式清理**不是**
|
|
115
|
+
「外键没开才要手工补」——`createStore` 走 `new DatabaseSync(path)` 不带选项,而 node:sqlite 的
|
|
116
|
+
`enableForeignKeyConstraints` 默认为真(C1 实测:新库上 `PRAGMA foreign_keys` 返回 1,删父行
|
|
117
|
+
确实级联清掉本表),所以 `ON DELETE CASCADE` 今天一直是**活的**。显式 DELETE 留着是为了不把
|
|
118
|
+
清理押在一份没人钉过的驱动默认值上:连接哪天以 `enableForeignKeyConstraints: false` 打开,
|
|
119
|
+
悬空链接只有那条 DELETE 挡得住(口径见 `src/store.js:278-283` 与 `:1387-1391` 的注释,
|
|
120
|
+
`deleteBySharePackId` 同处置)。本库一共**两条**外键,都是活的——这一条与
|
|
121
|
+
`recall_evals(recall_run_id) → recall_runs(id)`;此前本节写的 `recall_events` 那张表并不存在。
|
|
122
|
+
声明式约束因此既是意图记录、也是今天实际生效的执行,两边不矛盾。
|
|
118
123
|
|
|
119
124
|
`reclaim` 报告因此从两项变三项:
|
|
120
125
|
|
|
@@ -160,3 +165,60 @@ cleared: { dream_run_inputs, archived_embeddings, distill_records }
|
|
|
160
165
|
- **启动期裁剪**:读装配后的 cfg(面板改了保留期也认),且默认关时不动手;
|
|
161
166
|
- **白名单与往返**:两个键过 `settings` 校验(含范围外整数被拒),PUT 往返后 effective 读得回;
|
|
162
167
|
- **轻量模式**:预设关 `enabled`、保留用户显式设的 `retentionDays`、不污染用户对象。
|
|
168
|
+
|
|
169
|
+
## 10. 读出口:`/memory/provenance` 与 `distillRecords.enabled` 门(#421 写、#509 整改)
|
|
170
|
+
|
|
171
|
+
两张表自 #399 起只有写入路径:库里有答案、`service.provenanceFor` 能查,外部拿不到——
|
|
172
|
+
「这条记忆是从哪几句蒸出来的」只能开 sqlite 手写 JOIN。#421 补上读出口,两个入口共用
|
|
173
|
+
一份实现:
|
|
174
|
+
|
|
175
|
+
- 宿主内面板路由 `GET /api/dsh-mneme/memory/provenance`(`policy: token`);
|
|
176
|
+
- Bearer 数据面 `GET /memory/provenance?id=<memoryId>`(daemon 与宿主外部访问同享)。
|
|
177
|
+
|
|
178
|
+
口径只写在 `src/provenance-http.js` 一处,两个壳都调它——#411 已经为 reclaim / share 各
|
|
179
|
+
踩过一次「两张皮整段复制必然漂移」,`test/provenance-http.test.js` 的静态锁把「错误码字面
|
|
180
|
+
量与开关判据只许出现在本模块、两个壳都真的把 `config` 转交进来、读侧判据与写侧
|
|
181
|
+
`src/summarize.js:988` 逐字同源」写成断言。
|
|
182
|
+
|
|
183
|
+
**形状**:一个链接一个窗口 `{session_id, seq_start, seq_end, created_at, records[]}`,
|
|
184
|
+
`records` 是区间内的逐条会话原文(按 `seq` 升序、不越界)。没有链接的记忆返回**空
|
|
185
|
+
`windows`**——「不是蒸馏来的」是有效答案,不是错误;未知 id 同样回空 `windows`(读端点
|
|
186
|
+
不替调用方判断记忆是否存在)。到期裁剪后链接保留、指向空区间,面板据此显示「原文已过期」。
|
|
187
|
+
|
|
188
|
+
**开关门(#509 评审整改)**:读出口吃 `distillRecords.enabled`——与写侧**同一枚开关、同一
|
|
189
|
+
个判据表达式**(`config?.distillRecords?.enabled === true`,读侧唯一一份在
|
|
190
|
+
`isDistillRecordsEnabled`)。此前全树只有 `src/index.js:270`(启动期裁剪)与
|
|
191
|
+
`src/summarize.js:988`(写入)行为性读它,读出口一处都不读,于是「把 distill_records 关
|
|
192
|
+
掉」的实际语义是*不再写、也永久不再删*,却还多一个能把历史原文读出去的口。状态码阶梯:
|
|
193
|
+
|
|
194
|
+
| 状态码 | 含义 | 为什么不是别的 |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `401` | 没带 token | 两个壳各自的门,在共用模块之前 |
|
|
197
|
+
| `400 {"error":"missing-id"}` | 缺 `id` | 语法畸形,与服务端状态无关,所以排在功能开关之前(与 reclaim 的 `invalid-json` 同档) |
|
|
198
|
+
| `409 {"error":"distill-records-disabled"}` | 用户关了 `distillRecords.enabled` | 与 `src/api.js:525` 的 `profileExtractEnabled` 同型:**用户关了 = 409,装配坏了 = 503**,两个答案必须可区分(`src/index.js:589-591`) |
|
|
199
|
+
| `503 {"error":"provenance-unavailable"}` | service 上没这两个方法(headless / 旧装配) | 「没接线」不是「这条记忆没有来源」,两种空必须分开读数 |
|
|
200
|
+
| `200` | `{memoryId, windows}` | 空 `windows` = 不是蒸馏来的 |
|
|
201
|
+
|
|
202
|
+
**为什么不是 404**:本端点被明确设计成永远不就「这条记忆是否存在」作断言(缺 id 是 400、
|
|
203
|
+
未知 id 是 200 + 空 windows),而 404 在本仓已有稳定含义——未知路由与资源不存在
|
|
204
|
+
(`/memories/:id`、`entity-not-found`)。把「功能关了」塞进 404,调用方会在状态码这个
|
|
205
|
+
被代理 / 监控 / 重试逻辑唯一读取的通道上,把它读成「这条记忆不存在」;那两种误读的后续
|
|
206
|
+
动作完全不同(一个去开开关,一个去查这条记忆)。409 也不给 200 的形状加说明性键——响应
|
|
207
|
+
体只多一个 `windows`,开关状态走状态码通道。
|
|
208
|
+
|
|
209
|
+
**生效节奏**:`cfg` 是启动期冻结值(面板改开关与 `panel_mode` 一样在下次启动生效),读写
|
|
210
|
+
两侧取的是同一份快照,所以两道门永不分歧;该键在 settings 白名单与 `NESTED_FLAG_PATHS`
|
|
211
|
+
里都登记着,面板会显示「待重启」(#481「面板开着、功能却不注入」的教训)。
|
|
212
|
+
|
|
213
|
+
**daemon 这一面**:`src/serve.js` 把 `feature_flags` 里的 `distillRecords.enabled` 展开进
|
|
214
|
+
cfg(与宿主 `index.js:217-233` 同款展开,只取蒸馏这一组),与宿主共用同一个 memory.db 与
|
|
215
|
+
同一份 kv——面板开着,daemon 就读得到。残留不对称:daemon 结构上看不到宿主的 bundle
|
|
216
|
+
config,所以「只写在部署侧 bundle 配置里打开、从没在面板点过」的用户,宿主认、daemon 不
|
|
217
|
+
认(每个请求回 409)。这不是故障,写进 `docs/DAEMON.md` 的那条已知边界。
|
|
218
|
+
|
|
219
|
+
**关开关 ≠ 删除已留存原文**(本轮之后唯一诚实的表述):这道门只封 HTTP 读路径。关掉后
|
|
220
|
+
`src/index.js:270` 跳过启动期裁剪,已留的 `distill_records` 行永久冻在盘上,任何能摸到
|
|
221
|
+
memory.db 的进程仍可读;唯一出路是 `dsh-mneme reclaim`(`src/maintenance.js` 不读这个
|
|
222
|
+
键,手动可删)或重新开启后的启动裁剪。把「关 = 删掉」写进任何文档都是谎话。后续可做的
|
|
223
|
+
是让裁剪与写开关解耦(`retentionDays` 有值就裁,无论 `enabled`),那是让开关语义完全诚实
|
|
224
|
+
的唯一修法,超出本轮范围。
|