SQLite 漏读检查点
SQLite saver 读 DeltaChannel 历史为空,检查父检查点 ID 排序与 parent_checkpoint_id;核对 3.1.2 递归祖先读取修复。
LangGraph 使用 SQLite 保存检查点,读取 DeltaChannel 历史却得到空的 writes,祖先里明明还保存着初始值。这时要沿 parent_checkpoint_id 检查实际父子关系,尤其是父检查点的 ID 按字符串排序反而大于子检查点时。
这条缺口出现在 langgraph-checkpoint-sqlite 3.1.1 的同步、异步读取路径中。已核对的正式 3.1.2 改为沿父指针读取祖先;只升级 LangGraph 主包,不能证明实际装入的 SQLite saver 已更新。
父子关系被 ID 范围挡住
可以把错误关系简化为父 ID z-older、子 ID a-newer。子检查点明确指向这个父检查点,但旧查询先筛选 checkpoint_id <= target_id,再按 ID 倒序读取。父 ID 超出子 ID 的范围,就没有机会进入祖先重放。
结果可能没有异常:需要的 seed 和 pending writes 被漏掉,读取返回 {'writes': []}。这是读取路径的问题,不能仅凭空列表判断用户从未写过数据。
通常的检查点 ID 带时间顺序,但不同进程的时钟可能破坏跨进程排序。已有自定义 saver、导入历史或人工测试 ID,也需要按真实父链核对。这里的示例只是说明过滤条件,不是在建议替换框架的 ID 生成规则。
保存目标检查点配置、thread、namespace 和祖先关系,比较 SQLite 读取结果与已有完整状态记录。不要为了让字符串顺序好看而批量重命名 ID:其他检查点、任务和业务记录可能仍引用它们。
仅去掉范围条件还不够
旧读取器要先在结果流里找到目标,再处理祖先。如果只删掉范围过滤,仍按 ID 倒序返回,排序更大的父检查点可能先到;目标还未出现,父记录就被跳过。
正式 3.1.2 的查询从目标检查点开始,用递归 CTE 追踪 parent_checkpoint_id,并同时限制 thread 和 namespace。结果按“目标、父、祖父”的链路送入读取器,随后把合法更新整理为从旧到新的重放顺序。
目标自身的 seed 和写入不属于这次祖先历史返回范围。核对输出时,应按 saver 的契约检查祖先,不能为了凑出预期列表,把目标自己的更新又加一次。
沿指针读取还要处理异常循环。SQLite 的保存操作可能替换已有 ID;若错误地把它挂到自己的后代之下,父链会回到已访问的检查点。正式实现有重复 ID 停止条件,但它只防止读取无休止循环,不会替应用修好错误父链。
更新 saver,保留原历史验收
按项目原来的依赖约束选择兼容的正式 SQLite saver,例如 3.1.2,并核对部署中真正使用的包、连接与同步或异步入口。更新前保留检查点数据库和配置,不直接修改内部 SQL 或删除被漏读的祖先。
在获准的测试环境中保留同一父子关系,分别检查同步 get_delta_channel_history 与异步 aget_delta_channel_history。实际祖先中的初始值应作为 seed 返回,后续合法写入不应因 ID 排序被遗漏;正常顺序的检查点也应保持原有结果。
还要核对 thread 与 namespace,没有读取到另一条历史。修复针对 SQLite 的祖先遍历,不负责补回已经被清理、损坏或从未保存的数据,也不能据此承诺所有自定义 saver 都支持同样的 ID 条件。


