问题与影响
对只存在于事件日志中的 Todo,complete_goal_todo 会把 completion_turn_key 写入 TODO_COMPLETED 事件;但重新构建 Todo 投影后,这个 key 会丢失。
因此,原 Turn 在“完成事件已持久化、外层 journal/checkpoint 尚未完成”之后进行恢复重试时,会被错误地当成另一个 Turn 的冲突重放。Markdown Todo 路径支持的 same-turn 幂等恢复,在 event-sourced 路径上失效。
预期行为
- 使用原
completion_turn_key 重试:返回 idempotent_replay=true、changed=false,且不追加新事件。
- 使用不同
completion_turn_key 重试:继续 fail closed,拒绝跨 Turn 冒领完成结果。
实际行为
第一次完成成功;使用相同 key 重试时抛出:
ValueError: todo is already completed under a different completion_turn_key
最小合成复现
环境:LoopX main commit 13143e9bd98b,Python 3.12.13,不依赖特定 host。
- 创建一个公开合成 Goal,Markdown Todo 区为空,并在其
events.jsonl 中追加一个 TODO_ADDED agent Todo。
- 调用
complete_goal_todo(..., completion_turn_key="turn-a", no_followup=True);结果为 completed=true、changed=true、source=event_log。
- 对同一 Todo 再次调用完全相同的完成操作,仍传入
completion_turn_key="turn-a"。
- 第 3 步抛出上述跨 Turn 冲突错误,而不是返回幂等 receipt。
疑似表面(假设)
loopx/event_sourced_state.py 中 TODO_COMPLETED 的 reducer 字段白名单没有保留已经持久化的 completion_turn_key;随后生成 Markdown read model 时也没有把该字段传给 Todo metadata renderer。完整调用路径最终让 completed_todo_replay 读到空 key。
一个最小修复可以只补齐该字段的 reducer/renderer round-trip,并增加 event-projected turn-a -> turn-a 幂等正例与 turn-a -> turn-b 拒绝负例;不需要新增 schema、store 或 authority policy。没有 key 的旧 completion 事件应继续 fail closed,不应推测或回填身份。
认领
我愿意认领这个 issue,并按上述最小范围提交修复与聚焦回归测试。如果维护者对兼容边界或测试落点有不同要求,我会先按反馈收窄方案再实现。
已完成验证
- 在干净的最新
main 上用临时、纯合成 fixture 稳定复现。
- 已有 Markdown 路径回归 #3049 明确要求“同一 Turn 幂等、不同 Turn 拒绝”。
- 已核对 open/closed issues 与 PR;未找到这个 event-projection same-turn replay 缺口的现有跟踪项。
- 当前相邻 PR #3195 和 #3074 均未修改
loopx/event_sourced_state.py 的该投影字段。
问题与影响
对只存在于事件日志中的 Todo,
complete_goal_todo会把completion_turn_key写入TODO_COMPLETED事件;但重新构建 Todo 投影后,这个 key 会丢失。因此,原 Turn 在“完成事件已持久化、外层 journal/checkpoint 尚未完成”之后进行恢复重试时,会被错误地当成另一个 Turn 的冲突重放。Markdown Todo 路径支持的 same-turn 幂等恢复,在 event-sourced 路径上失效。
预期行为
completion_turn_key重试:返回idempotent_replay=true、changed=false,且不追加新事件。completion_turn_key重试:继续 fail closed,拒绝跨 Turn 冒领完成结果。实际行为
第一次完成成功;使用相同 key 重试时抛出:
最小合成复现
环境:LoopX
maincommit13143e9bd98b,Python3.12.13,不依赖特定 host。events.jsonl中追加一个TODO_ADDEDagent Todo。complete_goal_todo(..., completion_turn_key="turn-a", no_followup=True);结果为completed=true、changed=true、source=event_log。completion_turn_key="turn-a"。疑似表面(假设)
loopx/event_sourced_state.py中TODO_COMPLETED的 reducer 字段白名单没有保留已经持久化的completion_turn_key;随后生成 Markdown read model 时也没有把该字段传给 Todo metadata renderer。完整调用路径最终让completed_todo_replay读到空 key。一个最小修复可以只补齐该字段的 reducer/renderer round-trip,并增加 event-projected
turn-a -> turn-a幂等正例与turn-a -> turn-b拒绝负例;不需要新增 schema、store 或 authority policy。没有 key 的旧 completion 事件应继续 fail closed,不应推测或回填身份。认领
我愿意认领这个 issue,并按上述最小范围提交修复与聚焦回归测试。如果维护者对兼容边界或测试落点有不同要求,我会先按反馈收窄方案再实现。
已完成验证
main上用临时、纯合成 fixture 稳定复现。loopx/event_sourced_state.py的该投影字段。