Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3E63DD?style=flat-square" alt="license"></a>
<a href="https://github.com/slow-stack/mneme/actions"><img src="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI" alt="CI"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-22%2B-3E63DD?style=flat-square&logo=nodedotjs&logoColor=white" alt="node"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-2577%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-2623%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://codecov.io/gh/slow-stack/mneme"><img src="https://img.shields.io/codecov/c/github/slow-stack/mneme/main?style=flat-square" alt="coverage"></a>
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome"></a>
</p>
Expand Down Expand Up @@ -270,7 +270,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790

```bash
cd dsh-mneme && npm install
npm test # 2577 个测试
npm test # 2623 个测试
npm run stress # 三轴线压测
npm run sync # src → lib 同步
```
Expand Down Expand Up @@ -550,7 +550,7 @@ Community projects that wire mneme into more platforms — not part of this repo

```bash
cd dsh-mneme && npm install
npm test # 2577 tests
npm test # 2623 tests
npm run stress # three-axis stress test
npm run sync # src → lib sync
```
Expand Down
10 changes: 10 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

## 🆕 新增

- **溯源读出口补上反方向:`GET /memory/provenance?sessionId=<sid>`——「这个会话蒸出了哪几个窗口」第一次可直接查,此前只能「从一条记忆往回找」(#524,从 #421 拆出方向 2)**:#421 当初拆成两个方向,方向 1(记忆 → 窗口 → `distill_records` 原文)由 #509 交付,方向 2(会话 → 它产出的蒸馏窗口)一直没做。底下其实早就齐了——`memory_provenance` 自 #399 就在写、`idx_memory_provenance_session` 自那时就建着——缺的只是入参方向:`parseProvenanceQuery` 只认 `?id=<memoryId>`,`session_id` 只在响应里躺着。**复用同一条路径而不是新开一条**:多一条路径就多四处要登记的地方(`SECURITY.md` 两张表、包 README 路由表、`test/security-doc.test.js` 的清单锁),而两个方向读的是同一批数据、只是入口不同;`SECURITY.md` 只登记路径不登记 query 形状,所以方向 2 在那两张表里一行都不用加。两个主键**互斥**:都缺 `400 missing-id`、都给 `400 ambiguous-id`——**刻意不做「都给则择一」**,择一意味着调用方可以发一个自己也不知道会被读哪个主键的请求,而两个方向的答复形状不同,那种静默选择只会在出事之后才被发现。

**`limit` 不是可选的礼貌参数,是这条方向的前置条件**:按会话读是这张脸里天然最容易放大的方向——一个会话几十个窗口、每个窗口都是整段逐字原文,不限量就是「按会话导出全量转录」,与 `/export`(一次拿全库)同量级只是换了形状,而它同样受门控。默认 20、上限 100,两个方向共用同一份解析:**只收十进制整数字面量**,`""` / `" "` / `0x10` / `1e3` / `"+1"` / `"12abc"` / 超 `Number.MAX_SAFE_INTEGER` 一律 400 `invalid-limit`——走 `Number()` 的话 `?limit=` 会静默变成 0 个窗口,一个畸形输入拿到最坏解释(`reclaim-http.js` 的 `parseReclaimQuery` 为同一理由立过这条规矩);越界也给 400 而不截到 100,因为「要 1000 个、实际给 100 还不吭声」比直接拒更糟。撞上限时答复带 `truncated: true`:**少了它,被截断的答复与「本来就只有这么多」逐字节相同**,调用方会把半份转录当成全部,那正是这张脸最容易骗人的地方。实现上 session 侧向 store 要 `limit + 1` 行、多出来那条只当探针不出口;memory 侧 `provenanceFor` 本来就全量返回,按长度判即可。

**方向 2 不能逐链接返回**:`memory_provenance` 的粒度是「一条记忆 → 一个窗口」,而一次蒸馏窗口通常产出**多条**记忆(`saveMemoryProvenance` 在 `src/summarize.js` 的写窗口循环里按条调用),照原样列会让「这个会话蒸了几次」这个唯一有意义的计数被重复行淹没。新增 `store.distillWindowsFor(sessionId, limit)` 走 `GROUP BY session_id, seq_start, seq_end`,带 `COUNT(*) AS link_count`——`link_count` 说明该窗口派生出几条记忆,是按窗口看的第二维;**刻意不回 `memory_id` 列表**,方向 1 已经是「记忆 → 窗口」,把 id 也塞进这一侧会让两个方向不再正交(同一份映射有两条读法就得有两处维护),真要逐条查仍走方向 1。`ORDER BY seq_start` 而不是 `created_at`:seq 是事件序号、就是会话内的先后,按它排才是「这个会话按时间蒸了哪几段」,按 `created_at` 会把同一批重跑的行打散。**`window_id` 也不能当分组键**——它是逐条记录生成的 `session:<id>:<seq>`(一行一个 seq),拿它分组会退化成「列原文」。

**503 改成按方向判**:`REQUIRED_METHODS` 分 `memory: [provenanceFor, distillRecordsFor]` / `session: [distillWindowsFor, distillRecordsFor]`,只缺 session 那侧方法时方向 1 仍然 200——把一个方向的问题报成整张脸坏了,会让人去查一个没坏的部署。409 与 503 的分工不动:前者是「用户关了 `distillRecords.enabled`」,后者是「装配没接线」。`provenanceReply` 的入参从 `{service, id, config, warn}` 改成 `{service, selector, config, warn}`,selector 由 `parseProvenanceSelector` 产出(两个壳都换成它,静态锁的 regex 同步改成 `selector: parsed.selector`)。**不动 `lib/client.js`**:面板侧本来就没有溯源读取 UI,这条方向是给 MCP / 代理 / CLI 用的。

回归测试 +11(`test/provenance-http.test.js` 21 条:两个主键都缺 / 都给 / 裁空白、`limit` 十二种畸形与越界、session 方向一行一窗口 + `link_count` + 顺序、两个方向各自的 `truncated`、503 按方向判、session 方向开关关也 409 且零 SQL)与 +3(`test/api.test.js`:`GROUP BY` 并行 + `link_count=2` + 跨会话不串、两主键都给 400、`limit=0` 400 与 `limit=2` truncated)与 +2(`test/standalone-api.test.js`:`?sessionId=` 列出窗口、两主键都给 400 且 `limit` 六种畸形全 400)。

- **会话级无痕闸门:指定会话与子代理「不进不出」,其余会话照常(#539,实现由 @wuchichiumike 在 #540 贡献)**:AgentTeams / subagent 的一次性会话也会拿到 Mneme 注入并写进同一个库,而此前能关的只有全局——关插件或关 `autoSummarize` 会连带打死别的会话,#471 的 scopeState 又是 agent/workspace 级政策、且本来就不阻断保存。这一版加的是**逐会话**判定:命中会话不注入记忆 / 用户设置 / 工具指引,不接受其 `memory_*` 与 `set_scope_state` 调用(拒绝发生在任何副作用之前),不对它蒸馏,压缩抢救 / M.O. 记账 / 自动提议三条写路径同样早退。

三个键全部 **opt-in**:`sessionGateEnabled`(默认 `false`,总闸)、`sessionGateSubagents`(默认 `true`,总闸开启时自动命中子代理)、`sessionGateSessions`(默认 `[]`,手动 id 名单,两者 OR)。`sessionGateEnabled` / `sessionGateSubagents` 进 `settings.js` 白名单并可走 `/features`(重启生效),`sessionGateSessions` 是数组、只能写在 bundle 配置。
Expand Down
8 changes: 4 additions & 4 deletions dsh-mneme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
[![tests](https://img.shields.io/badge/tests-2577%20passed-success)](https://github.com/slow-stack/mneme)
[![tests](https://img.shields.io/badge/tests-2623%20passed-success)](https://github.com/slow-stack/mneme)
[![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
[![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
[![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
Expand Down Expand Up @@ -535,7 +535,7 @@ dsh plugin --profile web add "C:/path/to/modusensus-dsh-mneme-0.9.0.tgz"
| `GET` | `/profile` | 用户画像,返回 `{profile:"..."}`;未设置时为空字符串 |
| `GET` | `/rules` | 行为规则列表,返回 `{rules:[...]}`;未设置时为空数组 |
| `GET` | `/memories?limit&offset&type&minImportance&source&order=chrono&include_archived&occurred_from&occurred_to` | 分页列出记忆(`include_archived=true` 含归档;occurred 时间窗按 `occurred_at` 回退 `created_at`) |
| `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) |
| `GET` | `/memory/provenance?id=<memoryId>` 或 `?sessionId=<sid>`(可选 `&limit=`,默认 20、上限 100) | 溯源读出口,两个方向共用一条路径(#421 拆出两条:方向 1 记忆→窗口由 #509 交付,方向 2 会话→窗口由 #524 补上;宿主内面板路由 `/api/dsh-mneme/memory/provenance` 同一口径)。**两个主键互斥**:都缺 `400 {"error":"missing-id"}`、都给 `400 {"error":"ambiguous-id"}`(刻意不静默择一——两个方向的结果形状不同,猜一个只会在出事之后才发现)。方向 1 是一条溯源链接一个窗口;方向 2 把同一个 `(session_id, seq_start, seq_end)` 合并成一项(`GROUP BY`),带 `link_count`=这个窗口派生出几条记忆,按 `seq_start` 排——逐链接返回会让「这个会话蒸了几次」被重复行淹没。两个方向都带 `truncated`(撞上 limit 时说「还有更多」;少了它,被截断的答复与「本来就这么少」逐字节相同)。`records` 是那段蒸馏窗口的**逐条会话原文**(按 `seq` 升序、不越出区间)。没有链接返回**空 `windows`**——「不是蒸馏来的」是有效答案,不是错误。状态码阶梯:`401` 无 token → `400` 缺参/多参/`invalid-limit` → `409 {"error":"distill-records-disabled"}`(`distillRecords.enabled` 关着;该开关是**启动快照**,面板改完要到下次启动生效,面板会显示「待重启」)→ `503 {"error":"provenance-unavailable"}`(该方向要用的方法没接线,**按方向判**——只缺 session 侧方法时方向 1 仍然 200)→ `200`。`409` 与 `503` 必须分开读数:前者是「用户关了」,后者是「装配坏了」。**关开关 ≠ 删掉已留存原文**——只是不再经 HTTP 提供,盘上的行要 `dsh-mneme reclaim`(或重新开启后的启动裁剪)才走,详见 [docs/DISTILL.md](docs/DISTILL.md) |
| `GET` | `/memories/:id` | 单条记忆 |
| `POST` | `/memories` | 新增记忆 `{type,title,content,importance?,tags?,source?,sensitivity?,occurred_at?,agent_scope?,workspace_scope?}`;响应含 `action: created\|merged` |
| `PUT` | `/memories/:id` | 局部更新 `{title?,content?,type?,importance?,tags?,reason?,agent_scope?,workspace_scope?}`;content 改写按 human_override 入档 |
Expand Down Expand Up @@ -719,7 +719,7 @@ src/
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
└── index.js # 插件接线
lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
test/ # 2577 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
test/ # 2623 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
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 运行时清单
```

Expand All @@ -728,7 +728,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
```bash
cd dsh-mneme
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
npm test # 运行 2577 个测试
npm test # 运行 2623 个测试
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
```
Expand Down
6 changes: 4 additions & 2 deletions dsh-mneme/docs/DAEMON.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ daemon 是**数据面**,不是第二个宿主:

## 2. 对外接口

路由面 = `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)。
路由面 = `src/api-standalone.js` 全表(health/status/profile/rules/memories 读写/`/memory/provenance`(记忆级 / 会话级溯源读出口,#421+#524)/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)。

`/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 这一面就读得到。
`/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` 缺参(`missing-id` / `ambiguous-id` / `invalid-limit`) → `409` 功能关了 → `503` 装配没接线(按方向判) → `200`。

两个读方向共用同一条路径(#524 补上方向 2):`?id=<memoryId>` 是「这条记忆派生自哪些窗口」,`?sessionId=<sid>` 是「这个会话蒸出了哪些窗口」;两个主键**互斥**,都缺与都给都回 400(择一等于让调用方发一个自己也不知道会读哪个主键的请求,而两边的结果形状不同)。方向 2 必须带 `limit`(默认 20、上限 100)——按会话读是这张脸里天然最容易放大的方向,一个会话几十个窗口、每个窗口都是整段逐字原文,不限量就是「按会话导出全量转录」,与 `/export` 同量级只是换了形状;撞上限时答复带 `truncated: true`,少了这个读数,被截断的答复与「本来就这么少」逐字节相同。方向 2 的窗口按 `(session_id, seq_start, seq_end)` 合并成一行 `GROUP BY`,带 `link_count`——一条蒸馏窗口通常产出多条记忆,逐链接返回会让「这个会话蒸了几次」被重复行淹没。daemon 读的是**面板持久化的开关**(`serve.js` 把 `feature_flags` 里的 `distillRecords.enabled` 展开进 cfg,与宿主 `index.js:217-233` 同款),与宿主共用同一个 memory.db、同一份 kv——所以用户在面板里开着,daemon 这一面就读得到。

残留不对称要知道:daemon 结构上看不到宿主的 **bundle config**(第一期不吃宿主 config schema),所以「只写在部署侧 bundle 配置里打开、从没在面板点过」的用户,宿主认、daemon 不认——daemon 会对每个请求回 409。这不是故障,是 daemon 装配面的已知边界;要让 daemon 也认,去面板里把开关显式打开一次(面板的「待重启」提示同样适用:开关是启动快照,改完要重启才生效)。

Expand Down
Loading
Loading