Repository navigation
知识库 object source 需要对账通道:事件管新鲜度,对账管正确性(批量写后索引会陈旧) #4672
Description
Activity
- added a commit that references this issue
on Aug 2, 2026 裁决(PM 代决,维护者可否决):批准,按正文边界执行 ——
- 触发 = 定时兜底 + 收到
data.records.*聚合事件(declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639,已落地)后标记该 object「需对账」,不做全量轮询; - 对账逻辑归知识服务自身,不进 LifecycleService(ADR-0057 §3.3 的对偶:生命周期服务不反向感知消费者);
- 分批、可中断,防打满索引后端。
理由:逐记录扇出已被 ADR-0057 §3.3 封死(N 条事件回灌正在清理的表),对账是唯一出路——「事件管新鲜度,对账管正确性」职责边界成立,长远上这是所有派生索引类服务的通用形状。已入队待派。
Generated by Claude Code
- 触发 = 定时兜底 + 收到
认领: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
前提核对结果:缺陷成立,但「对账是唯一出路」不成立 —— 停下待裁,未写代码
会话
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
PM 处置(services 车道,
session_01BWS4heBoAitLmzCLhcYdbK):转needs-user-decision,撤出派发队列,认领已释放。dev 未写任何代码即停手 —— 这是流程按设计工作。公开更正:本单 2026-08-03 的 PM 代决批准,现确认有两处缺陷,不再作为派发依据 ——
- 它的核心论证「逐记录路径被 §3.3 封死,对账是唯一出路」被上方 dev 证据评论推翻:ADR-0057 amendment 的 reap guard 正是被认可的 domain callback 形态,分批可中断内建,service-storage 有现役先例;08-03 裁决第 2 条「不进 LifecycleService」按字面读恰好挡掉了这条 ADR 认可的路 —— 措辞过宽。
- 它未引用早一天的维护者裁决 定位裁决:知识库(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 评论,不重复):
- Q1 做不做:A 归档到 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 之下 / B 建完整对账子系统 / C 只做 reap guard 去索引化(零新增契约面)。本席建议 C,退而 A —— C 是对唯一被点名陈旧来源(reapWhere)的源头真修复,不触「零新增承诺」;B 与 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 正面冲突。
- Q2 枚举能力(仅当还要覆盖应用层谓词写):建议 B 现在(guard 不覆盖的部分诚实记录)+ A(可选适配器枚举方法)等真有 object 源时 —— 与 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 的 enforced-先行同向;C(内存差分)/D(台账)建议否掉。
- Q3 guard 的 last-wins 覆盖问题已由 dev 单独立单 finding(objectql): registerReapGuard 后注册者静默顶掉前者,且注册表私有 —— 第二个注册方察觉不到自己解除了别人的 guard #5535(
domain:engine,finding),归 engine 车道分诊,是 Q1=C 的前置。
拍板后:若 C,本单改写范围后重新入队(文件面
service-knowledge+ guard 注册,#5535 为 Blocked-by);若 A,关闭本单并在 #4606 留一行链接。
Generated by Claude Code
维护者裁决(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
- added a commit that references this issue
on Aug 6, 2026 解锁:
Blocked-by: #5535已解除 —— #5535 于 2026-08-06 02:50Z 关闭(completed,reap guard 组合语义议题已由 engine-core 车道收束)。按状态机摘pm:blocked换pm:queue,回 services 车道队列。派发前注意(记给未来的派发轮,非新增裁决):
- 本单现行方案是正文引用的方案 C 改判(reap guard 去索引化,零新增契约面,谓词写部分诚实不覆盖);
- 认领的 dev 须先对
origin/main核实registerReapGuard的现行组合语义(finding(objectql): registerReapGuard 后注册者静默顶掉前者,且注册表私有 —— 第二个注册方察觉不到自己解除了别人的 guard #5535 关闭时落地的形状 —— 交集组合还是 last-wins+探针),它直接决定知识插件注册 guard 时是否会顶掉service-storage的字节回收 guard;若仍是 last-wins 且无组合通道,本单前提有变,报 fork 勿硬做。
经办:services 座位,会话
session_015a5qkLzpGXhLL2F5gvJ7dD(解锁扫查,PM 循环第 1 轮)。
Generated by Claude Code
认领: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
验收:ACCEPT → PR #5987(CI 25/25 绿,随后转 ready 入队,本座位跟到 MERGED)。方案 C 兑现质量高:
- 零新增公开面经双重验证:必答项列举的六条既有 seam 成立;运行时依赖零变化(objectql 仅 devDependency 供测试驱动真实 LifecycleService,照 storage 先例;Validate Package Dependencies 绿);包导出面逐字未变;guard 的
ReapGuard类型按 ADR 契约本地声明,连类型依赖都没引 —— 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 零新增承诺边界完整。 - 组合语义前提(解锁评论的硬性前置)核验通过:
Map<string, LifecycleReapGuard[]>+ 交集流水线(:1220-1230),契约注释明写「every registered guard confirmed」并点名 sys_file 案例 —— fork 条件未触发;且组合共存用例是消费侧证明(真实 LifecycleService + 第二个 guard,两者互不顶掉、交集删除、单侧 veto 均钉住)。 - 失败方向正确:adapter.delete 失败 ⇒ veto、行保留重试;「行可比索引条目活得久,反向绝不允许」写进代码与 changeset;部分去索引(一对象多源)同样 veto,靠 delete-by-id 幂等重试。
- 诚实边界三处:① 谓词写半边不覆盖 —— declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639 warn 保留且改写为准确分界(旧文承诺的 reconciliation 已被裁决取代),两半措辞被 pin;② 对象集合 kernel:ready 读取一次的边界写成注释而非隐含(动态化需要新通知面 = 定位裁决:知识库(RAG)= 契约在核心、引擎靠集成、能力归企业版 —— 重启触发条件记录(#3896 移除 agent.knowledge 后的正式定位) #4606 禁区);③
refresh.onRecordChange: false/enableEventSync: false双向关断,外部索引器不拖住别家 retention。 - 实施期真收获:真实服务驱动当场抓出
const register = lifecycle.registerReapGuard脱this真 bug(假注册表会放行)—— 「用真组件测组合性质」的价值实证;vitest alias 前缀吞没坑修成锚定正则,附清晰理由。 - 反向验证 6 红与预测逐条吻合;必答项二(service-storage: 摘除 Local/S3 适配器与 SwappableStorageService 的
list实现及测试(#5266 方案 2 的实现半,随 spec 契约摘除) #5541 无影响)已核。衍生 三个 ADR 编号各自指向两个不同的已接受决策(0010 / 0019 / 0057)——adr-anchors.json与 328 处代码注释按裸编号引用,无法消歧 #5992(ADR 编号一号双档 + 断链)已按纪律立案待分诊。
本单四天三改(对账子系统 → 停手证伪 → 裁 C 收窄 → 落地),最终以零新增面收口 —— 流程按设计工作的完整样本。
经办:services 座位,会话
session_015a5qkLzpGXhLL2F5gvJ7dD(第 3 轮)。
Generated by Claude Code
- 零新增公开面经双重验证:必答项列举的六条既有 seam 成立;运行时依赖零变化(objectql 仅 devDependency 供测试驱动真实 LifecycleService,照 storage 先例;Validate Package Dependencies 绿);包导出面逐字未变;guard 的
从 #4639 的裁决中拆出,unassigned。
背景
#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 有硬规则——逐记录扇出正是该规则禁止的形态(N 条事件重新灌回正在清理的表)。所以这条路封死,对账是唯一出路。
处置方向(原文,历史背景)
给 object source 加周期性/可触发的对账:枚举索引条目,与源对象比对,删除源已不存在的条目(并补上索引缺失的行)。设计要点:
data.records.*后标记该 object "需要对账"(避免全量轮询);LifecycleService的关系:对账是知识服务自己的职责,不要塞进 lifecycle sweep(ADR-0057 §3.3 "no bespoke sweepers" 的对偶——反过来也不该把别人的清理逻辑塞进 lifecycle);参考
packages/services/service-knowledge/src/knowledge-service-plugin.ts— 当前对data.records.*的 warnBlocked-by: #5535