Skip to content

feat(provenance): 溯源读出口补上方向 2——按 session_id 列该会话的蒸馏窗口(#524) - #546

Merged
modusensus merged 2 commits into
mainfrom
feat/524-provenance-session
Nov 2, 2026
Merged

modusensus merged 2 commits into
mainfrom
feat/524-provenance-session

Conversation

@modusensus

@modusensus modusensus commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

这个 PR 修什么

#421 把溯源读出口拆成两个方向,方向 1(记忆 → 窗口 → 蒸馏原文)由 #509 交付,方向 2(会话 → 它产出的那些蒸馏窗口)一直缺着。

底下其实早就齐了:memory_provenance 自 #399 就在写,idx_memory_provenance_session 自那时就建着。缺的只是入参方向——parseProvenanceQuery 只认 ?id=<memoryId>,session_id 只在响应里躺着。

现在:

GET /memory/provenance?id=<memoryId>          # 方向 1(不动)
GET /memory/provenance?sessionId=<sid>&limit= # 方向 2(新增)

怎么修

存储侧新增 store.distillWindowsFor(sessionId, limit):

SELECT session_id, seq_start, seq_end, MIN(created_at) AS created_at, COUNT(*) AS link_count
  FROM memory_provenance WHERE session_id = ?
 GROUP BY session_id, seq_start, seq_end
 ORDER BY seq_start, seq_end LIMIT ?

不能逐链接返回:这张表的粒度是「一条记忆 → 一个窗口」,而一次蒸馏窗口通常产出多条记忆,照原样列会让「这个会话蒸了几次」这个唯一有意义的计数被重复行淹没。link_count 说明该窗口派生出几条记忆。

两个分组键的选择都踩过坑:window_id 不能当分组键(它是逐条记录生成的 session:<id>:<seq>,一行一个 seq,拿它分组会退化成「列原文」);ORDER BY seq_start 而不是 created_at(seq 是会话内的先后,按 created_at 排会把同一批重跑的行打散)。

协议层(src/provenance-http.js,两个壳共用的唯一一份)加了三个导出:parseProvenanceLimit、parseProvenanceSelector、两个常量。provenanceReply 的入参从 {service, id, config, warn} 改成 {service, selector, config, warn},两个壳都换成 parseProvenanceSelector,静态锁的 regex 同步改成 selector: parsed.selector。

三处刻意的取舍

① 复用同一条路径,不新开一条。 多一条路径就多四处要登记的地方(SECURITY.md 两张表、包 README 路由表、test/security-doc.test.js 的清单锁),而两个方向读的是同一批数据、只是入口不同。SECURITY.md 只登记路径不登记 query 形状,所以方向 2 在那两张表里一行都不用加——这也正是「同一条路径」在文档面上的直接好处。

② 两个主键互斥,不静默择一。 都缺 400 missing-id、都给 400 ambiguous-id。择一意味着调用方可以发一个自己也不知道会被读哪个主键的请求,而两个方向的答复形状不同(memoryId / sessionId 各认领自己那一侧)——那种静默选择只会在出事之后才被发现。

③ limit 是前置条件,不是礼貌参数。 按会话读是这张脸里天然最容易放大的方向:一个会话几十个窗口、每个窗口都是整段逐字原文,不限量就是「按会话导出全量转录」,与 /export(一次拿全库)同量级只是换了形状,而它同样受门控。默认 20、上限 100;只收十进制整数字面量,"" / " " / 0x10 / 1e3 / "+1" / "12abc" 一律 400——走 Number() 的话 ?limit= 会静默变成 0 个窗口,一个畸形输入拿到最坏解释(reclaim-http.js 的 parseReclaimQuery 为同一理由立过这条规矩)。越界也给 400 而不截到 100:要 1000 个、实际给 100 还不吭声,比直接拒更糟。

两处顺手改准的既有语义

truncated 字段:少了它,被截断的答复与「本来就只有这么多」逐字节相同,调用方会把半份转录当成全部——那正是这张脸最容易骗人的地方。实现上 session 侧向 store 要 limit + 1 行、多出来那条只当探针不出口;memory 侧 provenanceFor 本来就全量返回,按长度判即可。store 侧只照 limit 取,不替调用方判断「还有没有」——那是协议层的事,判据只许有一份。

503 改成按方向判:REQUIRED_METHODS 分 memory: [provenanceFor, distillRecordsFor] / session: [distillWindowsFor, distillRecordsFor],只缺 session 那侧方法时方向 1 仍然 200。把一个方向的问题报成整张脸坏了,会让人去查一个没坏的部署。409 与 503 的分工不动:前者是「用户关了 distillRecords.enabled」,后者是「装配没接线」。

回归测试 +16

test/provenance-http.test.js +11(21 条全绿):两个主键都缺 / 都给 / 裁空白、limit 十二种畸形与越界、session 方向一行一窗口 + link_count + 顺序、两个方向各自的 truncated、503 按方向判(只缺 session 方法时 memory 仍 200)、session 方向开关关也 409 且零 SQL。

test/api.test.js +3:GROUP BY 并行 + link_count=2 + 跨会话不串、两主键都给 400 ambiguous-id、limit=0 400 与 limit=2 truncated。

test/standalone-api.test.js +2:?sessionId= 列出窗口、两主键都给 400 且 limit 六种畸形全 400。

验收

闸门 结果
全量 npm test 2623 tests / 2622 pass / 0 fail / 1 skipped(47s)
三个受影响文件单独跑 167 / 167 pass
npm run sync Synced 82 file(s)(kept client.js lib-only)
node scripts/check-sync.js ✓ src/ 与 lib/ 一致(82 个文件)
提交署名 无 AI 署名、无 Co-Authored-By、无 "Generated with"

文档

四处 + CHANGELOG,都写清了「为什么」而不只是「是什么」:

  • dsh-mneme/README.md 路由表:方法栏补 ?sessionId= 与 &limit=,说明栏补两方向共用路径、互斥 400、GROUP BY + link_count、truncated、503 按方向判;
  • dsh-mneme/docs/DISTILL.md §10:新增「两个读方向共用同一条路径」「为什么不新开一条路径」「limit 与 truncated」三段,状态码表补 ambiguous-id 与 invalid-limit 两行;
  • dsh-mneme/docs/DAEMON.md:路由面与状态码阶梯同步,并补一整段方向说明;
  • dsh-mneme/CHANGELOG.md:[Unreleased] / 🆕 新增 段首。

SECURITY.md 一处都没动——路径没变,两张清单按 path 比对,方向 2 不需要新增登记行。

给报告者

@modusensus 这条是你在 #524 里自己拆出来的(原文四件事:入参形状 / limit / 门与状态码 / 文档登记),四件都按你列的次序做了,逐条对应在上面「三处刻意的取舍」与「两处顺手改准的既有语义」里。

第 ① 件你留的是开放项(「都给 → 400 还是择一,须写死」)——我写死成 400 ambiguous-id,理由见上。第 ② 件你只说「必须有 limit」,我把默认值与上界也一起定死了(20 / 100),并且把「截断必须说出来」当成同一条诉求的组成部分,而不是可选的礼貌。

#524 关不关留给你——如果你在 #524 里还有第 5 件事想加,这条可以先不合。

Summary by CodeRabbit

  • 新功能
    • 溯源查询现支持按记忆或会话查看来源窗口;同一会话中的相同窗口会合并,并显示关联数量。
    • 查询结果按窗口顺序排列,可设置返回数量上限;结果不完整时会标记为已截断。
  • 错误修复
    • 补充查询参数校验:必须且只能提供一种查询标识,数量限制须为 1–100 的整数。
  • 文档
    • 更新接口说明及测试通过数。

#421 把溯源读出口拆成两个方向:方向 1(记忆 → 窗口 → 蒸馏原文)由 #509 交付,
方向 2(会话 → 它产出的蒸馏窗口)一直缺着。底下其实早就齐了:memory_provenance
自 #399 就在写、idx_memory_provenance_session 自那时就建着——缺的只是入参方向,
parseProvenanceQuery 只认 ?id=<memoryId>,session_id 只在响应里躺着。

复用同一条路径而不是新开一条:多一条路径就多四处要登记的地方(SECURITY.md 两张
表、包 README 路由表、security-doc 的清单锁),而两个方向读的是同一批数据,只是
入口不同;SECURITY.md 只登记路径不登记 query 形状,所以方向 2 在那两张表里一行
都不用加。

两个主键互斥:都缺 400 missing-id、都给 400 ambiguous-id。刻意不做「都给则择
一」——择一意味着调用方可以发一个自己也不知道会被读哪个主键的请求,而两个方向的
答复形状不同,那种静默选择只会在出事之后才被发现。

limit 不是可选的礼貌参数,是这条方向的前置条件:按会话读是这张脸里天然最容易放
大的方向(一个会话几十个窗口、每个窗口都是整段逐字原文),不限量就是「按会话导出
全量转录」。默认 20、上限 100,只收十进制整数字面量——不走 Number(),因为它把
"" / " " / 0x10 / 1e3 都化成数,于是 ?limit= 会静默变成 0 个窗口。撞上限时答复带
truncated:少了它,被截断的答复与「本来就只有这么多」逐字节相同。实现上 session
侧向 store 要 limit + 1 行、多出来那条只当探针不出口。

方向 2 不能逐链接返回:memory_provenance 的粒度是「一条记忆 → 一个窗口」,而一次
蒸馏窗口通常产出多条记忆,照原样列会让「这个会话蒸了几次」被重复行淹没。新增
store.distillWindowsFor 走 GROUP BY session_id, seq_start, seq_end,带
COUNT(*) AS link_count;ORDER BY seq_start 而不是 created_at(seq 是会话内的先后)。
window_id 也不能当分组键——它是逐条记录生成的 session:<id>:<seq>,分组会退化成
列原文。

503 改成按方向判:REQUIRED_METHODS 分 memory / session 两套,只缺 session 那侧
方法时方向 1 仍然 200——把一个方向的问题报成整张脸坏了,会让人去查一个没坏的部署。

provenanceReply 的入参从 {service, id, config, warn} 改成 {service, selector,
config, warn},selector 由 parseProvenanceSelector 产出,两个壳都换成它,静态锁的
regex 同步改成 selector: parsed.selector。

回归测试 +11(provenance-http:两个主键都缺/都给/裁空白、limit 十二种畸形与越界、
session 方向一行一窗口 + link_count + 顺序、两方向各自的 truncated、503 按方向判、
session 方向开关关也 409 且零 SQL)、+3(api:GROUP BY 并行 + link_count=2 + 跨会话
不串、两主键都给 400、limit=0 400 与 limit=2 truncated)、+2(standalone-api:
?sessionId= 列出窗口、两主键都给 400 且 limit 六种畸形全 400)。
Copilot AI lite review requested due to automatic review settings November 2, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 33 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2991b2d and 558a194.

Files selected for processing: 18
  • README.md
  • dsh-mneme/CHANGELOG.md
  • dsh-mneme/README.md
  • dsh-mneme/docs/DAEMON.md
  • dsh-mneme/docs/DISTILL.md
  • dsh-mneme/lib/api-standalone.js
  • dsh-mneme/lib/api.js
  • dsh-mneme/lib/provenance-http.js
  • dsh-mneme/lib/service.js
  • dsh-mneme/lib/store.js
  • dsh-mneme/src/api-standalone.js
  • dsh-mneme/src/api.js
  • dsh-mneme/src/provenance-http.js
  • dsh-mneme/src/service.js
  • dsh-mneme/src/store.js
  • dsh-mneme/test/api.test.js
  • dsh-mneme/test/provenance-http.test.js
  • dsh-mneme/test/standalone-api.test.js

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 35a79c1 and 2991b2d.

Files selected for processing: 18
  • README.md
  • dsh-mneme/CHANGELOG.md
  • dsh-mneme/README.md
  • dsh-mneme/docs/DAEMON.md
  • dsh-mneme/docs/DISTILL.md
  • dsh-mneme/lib/api-standalone.js
  • dsh-mneme/lib/api.js
  • dsh-mneme/lib/provenance-http.js
  • dsh-mneme/lib/service.js
  • dsh-mneme/lib/store.js
  • dsh-mneme/src/api-standalone.js
  • dsh-mneme/src/api.js
  • dsh-mneme/src/provenance-http.js
  • dsh-mneme/src/service.js
  • dsh-mneme/src/store.js
  • dsh-mneme/test/api.test.js
  • dsh-mneme/test/provenance-http.test.js
  • dsh-mneme/test/standalone-api.test.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Walkthrough

/memory/provenance 现在支持按记忆 ID 或会话 ID 查询。接口新增统一的参数校验、会话窗口聚合读取、结果数量限制和截断标记,并同步更新相关测试与文档。

Changes: 溯源读取

Layer / File(s) Summary
会话窗口存储与服务
dsh-mneme/lib/store.js, dsh-mneme/src/store.js, dsh-mneme/lib/service.js, dsh-mneme/src/service.js
新增按会话与序号区间聚合窗口的存储查询,返回最早创建时间和关联记忆数;服务对象新增对应的转发方法。
查询入口、响应与校验
dsh-mneme/lib/provenance-http.js, dsh-mneme/src/provenance-http.js, dsh-mneme/lib/api*.js, dsh-mneme/src/api*.js, dsh-mneme/test/*, dsh-mneme/README.md, dsh-mneme/docs/*, dsh-mneme/CHANGELOG.md, README.md
入口将 id、sessionId 和 limit 交给共用选择器。共用模块校验互斥主键和 limit,按查询方向检查可用方法,并返回窗口及 truncated。测试和文档覆盖新增行为;测试数量说明更新为 2623。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ApiRoute as API路由
  participant Selector as parseProvenanceSelector
  participant Reply as provenanceReply
  participant Service as createService
  participant Store as createStore
  ApiRoute->>Selector: 解析 id、sessionId 和 limit
  Selector-->>ApiRoute: 返回 selector
  ApiRoute->>Reply: 传入 selector 和配置
  Reply->>Service: 请求会话窗口
  Service->>Store: 调用 distillWindowsFor
  Store-->>Service: 返回聚合窗口
  Service-->>Reply: 返回窗口数据
  Reply-->>ApiRoute: 返回 sessionId、窗口和 truncated
Loading

Possibly related issues

  • slow-stack/mneme#524:该 issue 要求增加会话维度溯源读取,并设置结果上限;本次变更实现了会话查询、limit 限制和截断标记。

Possibly related PRs

  • slow-stack/mneme#509:该 PR 首次加入溯源读出口和共用 HTTP 模块;本次变更扩展了同一接口和模块,增加会话查询方向。

Suggested reviewers: z2ace0107

Merge Risk: Minimal · previous review 2991b

The session query appears ready to merge after normal checks. No unresolved issue requiring a change was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed 标题准确概括了主要变更:为溯源读出口新增按 session_id 查询蒸馏窗口的支持。
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 15 functions across 13 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@codecov

codecov Bot commented Nov 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI lite review requested due to automatic review settings November 2, 2026 07:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@modusensus
modusensus merged commit a43e7ee into main Nov 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants