Skip to content

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

Description

@os-zhuang

从 #4639 的裁决中拆出,unassigned。

2026-08-05 范围改判(维护者批准方案 C,见评论):本单收窄为「reap guard 去索引化」—— 知识插件注册 lifecycle reap guard,在行删除前按 id 去索引,零新增契约面、不碰 packages/objectql、不建对账子系统;应用层谓词写(updateMany/deleteMany)的部分诚实不覆盖(文档 + 既有 warn 说明),待真实 object 源出现再按 #4606 的 enforced-先行原则回看。原正文保留作背景。

Blocked-by: #5535(reap guard 需先获得交集组合语义,否则第二注册方静默顶掉 sys_file 字节回收)

背景

#4639 给谓词写(multi: true → IDataDriver.updateMany/deleteMany)加了诚实的聚合事件 data.records.updated / data.records.deleted,载荷是 { id, type, object, matched, userId?, timestamp }。

这修好了 webhook 通路(新增 opt-in 的 bulk_update / bulk_delete 触发器),但修不好知识同步,而且这不是 #4639 的实现留下的缺口——是驱动契约的性质:

  • 知识索引是逐记录投影;
  • matched: 40 不指代任何一条记录,KnowledgeService.handleRecordUpsert / handleRecordDelete 都需要记录体或 id,适配器也不接受谓词。

所以谓词写之后,该 object 的知识索引会陈旧,且这条订阅链路无法自愈。#4639 的实现里已经让它出声(knowledge-service-plugin.ts 对 data.records.* 打 warn 指明索引可能陈旧),但出声不是修复。

现实影响

最主要的陈旧来源是 LifecycleService 的保留期回收(packages/objectql/src/lifecycle/lifecycle-service.ts 的 reapWhere),它按谓词删记录。注意不能把它改成逐记录扇出:ADR-0057 §3.3 有硬规则——

the LifecycleService's own deletes/rotations are not audited or activity-logged (otherwise cleanup re-feeds the tables it is draining — the same self-audit trap ADR-0052 already guards). Aggregate one summary row at most.

逐记录扇出正是该规则禁止的形态(N 条事件重新灌回正在清理的表)。所以这条路封死,对账是唯一出路。

⚠️ 2026-08-05 核对:上句「对账是唯一出路」已被推翻 —— ADR-0057 amendment 的 reap guard 是被认可的 domain callback 形态,见核对评论。

处置方向(原文,历史背景)

给 object source 加周期性/可触发的对账:枚举索引条目,与源对象比对,删除源已不存在的条目(并补上索引缺失的行)。设计要点:

  • 触发方式:定时 + 收到 data.records.* 后标记该 object "需要对账"(避免全量轮询);
  • 与 LifecycleService 的关系:对账是知识服务自己的职责,不要塞进 lifecycle sweep(ADR-0057 §3.3 "no bespoke sweepers" 的对偶——反过来也不该把别人的清理逻辑塞进 lifecycle);
  • 成本控制:分批、可中断,不能在大对象上把索引后端打满。

参考

Blocked-by: #5535

Activity

  1. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    裁决(PM 代决,维护者可否决):批准,按正文边界执行 ——

    1. 触发 = 定时兜底 + 收到 data.records.* 聚合事件(declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639,已落地)后标记该 object「需对账」,不做全量轮询;
    2. 对账逻辑归知识服务自身,不进 LifecycleService(ADR-0057 §3.3 的对偶:生命周期服务不反向感知消费者);
    3. 分批、可中断,防打满索引后端。

    理由:逐记录扇出已被 ADR-0057 §3.3 封死(N 条事件回灌正在清理的表),对账是唯一出路——「事件管新鲜度,对账管正确性」职责边界成立,长远上这是所有派生索引类服务的通用形状。已入队待派。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮(services 车道)
    会话:session_01BWS4heBoAitLmzCLhcYdbK
    分支:claude/issue-4672-knowledge-reconciliation
    Worktree:objectstack-issue-4672
    域:domain:services
    文件面:packages/services/service-knowledge/**(plugin + 新对账模块 + 同包测试)、.changeset/*.md。⛔ 不碰 packages/objectql(lifecycle)—— 按 2026-08-03 裁决,对账归知识服务自身,不进 LifecycleService。


    Generated by Claude Code

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    前提核对结果:缺陷成立,但「对账是唯一出路」不成立 —— 停下待裁,未写代码

    会话 session_01BWS4heBoAitLmzCLhcYdbK,分支 claude/issue-4672-knowledge-reconciliation(无提交,worktree 已清理)。

    1. 技术缺口仍然真实(origin/main 已核对)

    • packages/services/service-knowledge/src/knowledge-service-plugin.ts:178-187 那条 warn 还在,没有人加过对账;git log origin/main -- packages/services/service-knowledge 自 declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639(f2445c9)以来无新提交。
    • reindexSource(knowledge-service.ts:126-183)只 upsert、不 delete。所以即使把已声明但没人调度的 refresh.cron(packages/spec/src/ai/knowledge-source.zod.ts:26-33)实现成定时全量重建,孤儿条目照样留着——正文说的「删除源已不存在的条目」不是任何现有机制的副产品。
    • 适配器契约面 = upsert(docs) / search(query, topK) / delete(ids) / healthCheck?(packages/spec/src/contracts/knowledge-adapter.ts)。没有枚举能力。孤儿 id 按定义在源里查不到,search 又受 query 与 topK 约束,所以孤儿集合无法从现有面推导——正文「枚举索引条目」这一步今天根本没有落脚点。

    2. 但正文的关键论证被推翻了

    正文说:reap 不能改成逐记录扇出(ADR-0057 §3.3),「所以这条路封死,对账是唯一出路」。

    ADR-0057 自己的 amendment 提供了这条路:reap guard。

    • LifecycleService.registerReapGuard(object, guard)(packages/objectql/src/lifecycle/lifecycle-service.ts:414-421),契约在 :320:Reaper 删除前把候选行本体交给 guard,guard 先做外部清理再返回它确认删除的 id,未返回的行本轮保留。
    • 分批可中断已经内建:guardedReap(:1083-1111)每批 500 行、每次 sweep 最多 20 批,排不完的顺延到下次 sweep。正文第三条成本要求由扩展点本身满足。
    • 它不是被 §3.3 禁止的形态,ADR 原文的判定就写在契约注释里:「a guard is a domain callback, not a second sweeper」——不往正在清空的表回灌 N 条事件。
    • 已有现役先例,且完全在 objectql 之外:service-storage 通过 ctx.getService('lifecycle') + duck-type 注册了两个 guard(storage-service-plugin.ts:320-337),做的事情是「行消失前回收外部字节」——与「行消失前按 id 去索引化」结构相同。

    也就是说:正文点名的最主要陈旧来源(reapWhere 的保留期回收)可以在源头关掉,不需要新子系统、不需要碰 packages/objectql、不需要动 spec,而且天然分批可中断。裁决第 2 条「不进 LifecycleService」按字面读会挡掉这条路,但它挡掉的其实是 ADR 自己认可的扩展点——这一条需要维护者重读。

    3. 定位冲突:#4606

    #4606(维护者 2026-08-02 10:09,status:parked)对知识库的定位裁决是:短期无项目需求、非主线任务,「现有架构姿态即终态……开源核心零新增承诺」,能力归企业版;重启需满足三条触发条件之一;真要加 spec 面则「enforced 先行——检索路径真读它之后才进 spec(ADR-0078)」。

    本 issue 建于同日 15:58,PM 批准于 08-03 06:24,两处都没有引用 #4606。按 AGENTS.md Prime Directive #13(推翻已记录的裁决本身是一次裁决),这里需要维护者显式表态。

    实测的业务拉力(两仓一起数):没有任何地方声明过 object 知识源。registerSource 的唯一调用点是插件自己的 boot 循环(knowledge-service-plugin.ts:107),喂给它的 options.sources 无人提供;new KnowledgeServicePlugin(...) 只出现在本包测试里;examples/app-todo 依赖了 @objectstack/service-knowledge 与 @objectstack/knowledge-memory 却从未 import;闭源 cloud 仓有真适配器 packages/knowledge-turso,但同样零 registerSource、零 contentFields。也就是说今天没有任何索引会陈旧,因为没有任何索引。

    补充一条量级判断:非 system 调用方其实看不到孤儿条目——applyPermissionFilter 会把 sourceRecordId 查不回来的 hit 丢掉(knowledge-service.ts:300-355)。孤儿的真实代价是 ① 占掉 topK 名额稀释真结果(过滤发生在适配器返回 topK 之后),② isSystem: true 调用方直通不过滤,③ 索引存储成本。而谓词更新导致的内容漂移则完全没有兜底:记录还在,RLS 复查放行,索引里是旧正文。

    4. 三个待裁问题

    Q1 现在做不做? A 归档到 #4606 之下(保留 #4639 那条 warn 作为诚实信号)· B 按 PM 裁决在开源核心建对账子系统 · C 只做 reap guard 去索引化(零新增契约面),其余归档。倾向 C,否则 A:C 对唯一被点名的陈旧来源是真修复,新增面几乎为零,不触碰 #4606 的「零新增承诺」;B 要在维护者刚判为 parked 的领域新建子系统 + 新增 spec 契约面。

    Q2 枚举能力怎么来?(只要还要覆盖应用层 updateMany/deleteMany)A 给 IKnowledgeAdapter 加一个可选的分页枚举方法(memory 用 Map、cloud 的 turso 一条 SQL、ragflow 走 list-documents,三个适配器都能实现;缺失可探测,服务可按 source 大声拒绝)—— spec 座位 · B 只做 reap guard,应用层谓词写不覆盖 · C 服务内存快照差分(不动 spec,但重启即失效、随对象规模膨胀,正是「声明 ≠ 落地」陷阱) · D 落一张 sys_knowledge_* 台账(第二份真相源,自己也会漂)。倾向 B 现在 + A 等到真有 object 源时(与 #4606 的 enforced-先行 同向);C、D 建议直接否掉。

    Q3 若走 guard 路线,registerReapGuard 是 last-wins 且注册表私有,第二个注册方无法察觉自己解除了 sys_file 的字节回收 guard(对比 registerRetentionFloor 刻意做成可组合键)。已单独记录为 #5535,建议改成交集组合(全部确认才删)——objectql 座位,是 Q1=B/C 的前置。


    Generated by Claude Code

  5. removed their assignment
    on Aug 5, 2026
  6. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    PM 处置(services 车道,session_01BWS4heBoAitLmzCLhcYdbK):转 needs-user-decision,撤出派发队列,认领已释放。dev 未写任何代码即停手 —— 这是流程按设计工作。

    公开更正:本单 2026-08-03 的 PM 代决批准,现确认有两处缺陷,不再作为派发依据 ——

    1. 它的核心论证「逐记录路径被 §3.3 封死,对账是唯一出路」被上方 dev 证据评论推翻:ADR-0057 amendment 的 reap guard 正是被认可的 domain callback 形态,分批可中断内建,service-storage 有现役先例;08-03 裁决第 2 条「不进 LifecycleService」按字面读恰好挡掉了这条 ADR 认可的路 —— 措辞过宽。
    2. 它未引用早一天的维护者裁决 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606(知识库 parked、开源核心零新增承诺、spec 面 enforced 先行)。按 Prime Directive [WIP] Add Chinese version of the documentation #13,在 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 之下重启本单需要维护者显式表态,PM 代决不够格。

    待拍板(完整证据与三轴分析见上方 dev 评论,不重复):

    拍板后:若 C,本单改写范围后重新入队(文件面 service-knowledge + guard 注册,#5535 为 Blocked-by);若 A,关闭本单并在 #4606 留一行链接。


    Generated by Claude Code

  7. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    维护者裁决(2026-08-05,经 services 车道 PM 转达):批准方案 C —— 仅做 reap guard 去索引化(知识插件注册 lifecycle reap guard,行删除前按 id 去索引),零新增契约面、不碰 packages/objectql、不建对账子系统;应用层谓词写部分诚实不覆盖,待真实 object 源出现再按 #4606 的 enforced-先行原则回看(Q2 = B 现在 + A 待触发,同案批准)。本裁决在 #4606「零新增承诺」边界内执行,不构成对该裁决的推翻。

    状态:摘 needs-user-decision,转 pm:blocked —— 正文已加机器行 Blocked-by: #5535(reap guard 的交集组合语义是本单前置,否则知识 guard 作为第二注册方会静默顶掉 storage 的字节回收 guard)。#5535 已晋级 engine 队列;它落地后本单自动回队,按收窄后的范围(见正文顶部改判注记)派发。

    08-03 的 PM 代决(对账子系统方案)由本裁决正式作废。


    Generated by Claude Code

  8. hotlong commented on Aug 6, 2026

    @hotlong
    Contributor

    解锁:Blocked-by: #5535 已解除 —— #5535 于 2026-08-06 02:50Z 关闭(completed,reap guard 组合语义议题已由 engine-core 车道收束)。按状态机摘 pm:blocked 换 pm:queue,回 services 车道队列。

    派发前注意(记给未来的派发轮,非新增裁决):

    经办:services 座位,会话 session_015a5qkLzpGXhLL2F5gvJ7dD(解锁扫查,PM 循环第 1 轮)。


    Generated by Claude Code

  9. self-assigned this
    on Aug 6, 2026
  10. hotlong commented on Aug 6, 2026

    @hotlong
    Contributor

    认领:PM 循环第 3 轮(services 车道)
    会话:session_015a5qkLzpGXhLL2F5gvJ7dD
    分支:claude/issue-4672-knowledge-reap-guard(⚠️ 新分支名对应收窄后的方案 C 范围;前认领分支 claude/issue-4672-knowledge-reconciliation 已由前会话释放、零提交)
    Worktree:objectstack-issue-4672
    域:domain:services
    文件面:packages/services/service-knowledge/**(plugin 的 guard 注册 + 同包测试)+ changeset;⛔ 不碰 packages/objectql(裁决明文)、不碰 packages/spec / IKnowledgeAdapter 契约面(#4606 零新增承诺边界)
    串行约束已清:Blocked-by #5535 已解除(02:50Z 闭);PM 预核:origin/main 上 reapGuards 已是 Map<string, LifecycleReapGuard[]> 数组组合形态(lifecycle-service.ts:363/:480-483/:1092)—— dev 照解锁评论完整核验交集语义后开工;同包无其它在飞

    执行依据:维护者 2026-08-05 批准的方案 C(reap guard 去索引化;08-03 PM 代决已作废勿引用);「今天零 object 源」的事实已在裁决时计价,不构成 premise-false。派发令随后。


    Generated by Claude Code

  11. hotlong commented on Aug 6, 2026

    @hotlong
    Contributor

    验收:ACCEPT → PR #5987(CI 25/25 绿,随后转 ready 入队,本座位跟到 MERGED)。方案 C 兑现质量高:

    本单四天三改(对账子系统 → 停手证伪 → 裁 C 收窄 → 落地),最终以零新增面收口 —— 流程按设计工作的完整样本。

    经办:services 座位,会话 session_015a5qkLzpGXhLL2F5gvJ7dD(第 3 轮)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions