Skip to content

feat(service-knowledge): reap guard 去索引化 —— 保留期回收删行前先删索引条目 (#4672) - #5987

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-4672-knowledge-reap-guard
Aug 6, 2026
Merged

feat(service-knowledge): reap guard 去索引化 —— 保留期回收删行前先删索引条目 (#4672)#5987
hotlong merged 1 commit into
mainfrom
claude/issue-4672-knowledge-reap-guard

Conversation

@hotlong

@hotlong hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #4672

执行的是 2026-08-05 维护者批准的方案 C(reap guard 去索引化),不是正文原始的对账方案 —— 08-03 的 PM 代决已由该裁决作废,本 PR 不引用它。

问题

object 知识源是逐记录投影(索引条目经 sourceRecordId 指向行),而 LifecycleService 的保留期回收按谓词删行;ADR-0057 §3.3 又禁止把它扇出成 N 条逐记录事件(清理会回灌正在清空的表)。结果:行删了,文档还在 —— 孤儿。孤儿不只是浪费存储:权限过滤发生在适配器返回 topK 之后,所以它会占掉名额稀释真结果,而 isSystem 调用方直接读到它。

做法

KnowledgeServicePlugin 为每个被 object 源投影的对象注册一个 ADR-0057 reap guard:sweep 在删除前把候选行交给 guard,guard 按 id 删除对应文档(sourceId:recordId),只确认去索引成功的行

  • 失败方向(已用测试钉住):adapter.delete 失败 ⇒ veto,该行本轮保留,下轮 sweep 重试。允许「行比索引条目活得久」,绝不允许反过来 —— 与 service-storage 字节回收 guard 同构。
  • 组合语义:guard 按交集组合(finding(objectql): registerReapGuard 后注册者静默顶掉前者,且注册表私有 —— 第二个注册方察觉不到自己解除了别人的 guard #5535),既不会顶掉 service-storagesys_file 字节回收 guard,也不会被其顶掉。
  • 分批可中断:沿用 sweep 自身的约束(每批 500 行、每轮 20 批),本单无需自建成本控制。
  • 退出开关:源上的 refresh.onRecordChange: false 或插件的 enableEventSync: false 会同时关掉两个方向的内联同步(事件订阅 + reap 去索引)—— 复用既有开关,不新造第二个语义。

零新增公开面(#4606 边界)

新增契约面 = 。逐项列出所经过的既有 seam:

seam 归属 是否既有
ctx.hook('kernel:ready', …) @objectstack/core PluginContext 既有(本插件已在用)
ctx.getService('lifecycle') + duck-type 同上 既有(service-storage 现役先例)
registerReapGuard(object, guard) LifecycleService 既有,未改一行 objectql
IKnowledgeService.listSources() / getAdapter() packages/spec/contracts 既有,未加成员
IKnowledgeAdapter.delete(ids, ctx) 同上 既有,未加成员
AdapterContext.reason 取值 'lifecycle-reap' 同上 既有的开放联合((string & {}),注释即「caller-supplied free-form diagnostics tag」),非新增键

未新增 spec 键、未动 IKnowledgeAdapter、未动 packages/objectql、未建对账子系统、未做任何适配器枚举能力(Q2 = B 现在)。包的 index.ts 导出面逐字未变。

诚实不覆盖的那一半

应用层谓词写(调用方自己的 multi: true)仍不覆盖。#4639 的 warn 保留,并改写为准确描述这条分界(原文承诺的「reconciliation tracked in #4672」已被裁决取代,留着就是过度承诺)。content/docs/protocol/knowledge.mdx 同步改正。

另有一处边界写进代码注释而非隐含:对象集合在 kernel:ready 读取一次;boot 后经 registerSource 新增的、boot 时没有任何源的对象不被守护(已有 guard 的对象则会被覆盖,因为 guard 每次调用重新解析目标)。让这个集合动态化需要在 service 上新增通知面,正是 #4606 在真实 object 源出现前排除的。

前提核验(开工第一步,实测)

  1. 组合语义已是交集,fork 未发生:lifecycle-service.ts:363Map< string, LifecycleReapGuard[] >,:480-487 追加注册(同一函数重复注册是 no-op),:492-493 复制读取,:1220-1230batchedReap 中按「收窄流水线」求交 —— 契约注释明写「an id is deleted only if every registered guard confirmed it」,且明确点名第二注册方(按 id 去索引派生索引)不得顶掉 sys_file 字节回收。符合前置,继续。
  2. 「今天零 object 源」属裁决时已计价的已知事实,不构成 premise-false;guard 注册路径由本包测试 harness(插件 sourcesregisterSource)驱动验证。

反向验证(方向判定,再执行)

预判:摘掉 guard 注册 ⇒ 去索引/注册类断言转红,而「无源对象不受影响」「退出开关」「无 lifecycle 静默跳过」「#4639 warn 仍在」四类保持绿(它们断言的是「知识侧什么都没发生」,在没有修复时同样成立)。

实测与预判一致:6 红 / 31 绿,红的恰好是 6 条去索引与注册断言。

测试

packages/services/service-knowledge 新增 10 例 + 既有文件加 1 例,共 37 passed。覆盖裁决点名的五项:① reap 中按 id 去索引并确认;② adapter 失败 ⇒ veto(含跨两轮 sweep 仍不删);③ 与另一 guard 共存、两者都执行且互不顶掉;④ 无源对象 reap 不受影响;⑤ #4639 warn 仍在,且两半措辞都被钉住。

测试驱动的是真实 LifecycleService(devDependency,照 service-storage 先例),不是 lifecycle 的替身 —— #5535 的交集语义是消费侧属性,用假注册表只能证明假注册表会求交(即「因为什么都没产生所以通过」的空绿)。这个选择当场见效:真实服务立刻暴露了 const register = lifecycle.registerReapGuard 脱离 this 的真 bug,假注册表会放行。

新增的假引擎按 check:engine-double-contract 要求以 assertEngineDeleteDispatch(options) 开头,并已确认被该门发现并计为 pinned(不是「没扫到所以绿」)。

#5541 的影响

无。#5541 摘的是 service-storage 的 list 面(Blocked-by spec #5540),本 PR 不碰 service-storage、不碰 spec,只在 lifecycle 上多注册一个 guard;交集组合保证两个 guard 互不影响。已确认。


Generated by Claude Code

…ps them (#4672)

A knowledge index built from an `object` source is a per-record projection keyed
by `sourceRecordId`. `LifecycleService`'s retention reap deletes rows by
predicate, and ADR-0057 §3.3 forbids fanning that out as N per-record events
(cleanup must not re-feed the tables it is draining), so the row disappeared and
its document stayed: an orphan that still consumes a `topK` slot ahead of the
permission filter and is read straight through by an `isSystem` caller.

`KnowledgeServicePlugin` now registers an ADR-0057 reap guard for every object an
`object` source projects. The sweep hands the guard its candidate rows before
deleting them; the guard deletes each row's document by id and confirms only the
rows it de-indexed. A failing `adapter.delete` VETOES the row — it is kept and
retried next sweep, so a row may outlive its index entry but never the reverse.

Zero new contract surface (#4606's zero-addition boundary): no spec key, no
`IKnowledgeAdapter` member, no `packages/objectql` change. It is reached through
the same pre-existing seams `service-storage` uses for `sys_file` byte reclaim —
duck-typed `ctx.getService('lifecycle')` + `registerReapGuard`. Guards compose by
intersection (#5535), so this second registrar cannot displace that byte reclaim.

Application-level predicate writes stay uncovered, deliberately: #4639's warn is
kept and rewritten to name that line honestly instead of promising a
reconciliation pass that the 2026-08-05 ruling replaced.

Tests drive the REAL `LifecycleService` rather than a stand-in, because the
composition property under test belongs to the consumer — and it earned that
immediately, catching a `this`-binding bug a fake registry would have passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 1:57pm

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-knowledge.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/knowledge-rag.mdx (via @objectstack/service-knowledge)
  • content/docs/protocol/knowledge.mdx (via @objectstack/service-knowledge)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-knowledge)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 6, 2026
@hotlong
hotlong marked this pull request as ready for review August 6, 2026 14:23
@hotlong
hotlong added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 44106d9 Aug 6, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-4672-knowledge-reap-guard branch August 6, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

知识库 object source 需要对账通道:事件管新鲜度,对账管正确性(批量写后索引会陈旧)

2 participants