fix(goal): recover same-turn terminal closeout - #3261
Conversation
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
精确评审头(Exact Head): 3261@53dbfbae148965c62c8119282aa453346fe0ac8c
Base: main | 类型: runtime behavior(8 files, +269/-17) | 作者: huangruiteng(owner)
详细中文评审
动机
同一 Turn 若先完成了普通 Todo completion(no_followup 未置位),随后的重试会被幂等重放拦截,无法再追加 terminal no-follow-up 状态;当 writeback 与 quota spend 都已成功后,该 Turn 可能被“困”在缺少终态结算的中间态。PR 为“原始完成 Turn”提供受支持的 append-only 路径,补记缺失的 terminal closeout,同时保持幂等并阻止跨 Turn/替换 successor 的越权升级。
改动思路
把“普通已完成重放”与“same-turn terminal upgrade”显式分叉:completed_todo_replay 与 complete_event_projected_goal_todo 都先判定 terminal_upgrade_requested(status=done + no_followup + 当前 no_followup != true),再施加两道护栏——必须提供且匹配原 completion_turn_key;不得替换已存在的 successor。升级时保留原 completed_at,只追加一条带 completion_turn_key/no_followup=true 的不可变完成事件与独立 rollout receipt(status=terminal_no_followup,idempotency 字段含 status),重放仍幂等。
具体改动
loopx/cli_commands/todo_event.py(+18/-2):append_todo_rollout_event识别terminal_closeout(turn + complete + no-follow-up),status 写terminal_no_followup,idempotency 增加status使两次事件(ordinary / terminal)各有独立身份。loopx/control_plane/todos/completion_fence.py(+22/-2):completed_todo_replay新增no_followup参数与 terminal upgrade 护栏(原 turn key 必配、successor 不可替换),升级时返回None以走真实 append 路径。loopx/control_plane/todos/event_writeback.py(+25/-8):complete_event_projected_goal_todo支持 terminal upgrade——不提前返回幂等重放、保留completed_at、no_followup=true写入 payload 与投影、metadata_updated/changed反映升级、追加事件。loopx/control_plane/todos/summary_item.py(+1)、loopx/event_sourced_state.py(+2):completion_turn_key进入 summary 白名单与事件投影/渲染。loopx/todos.py(+1/-1):透传no_followup到 fence。- 测试(+200):
test_quota_settlement_cli覆盖“普通完成 → spend → terminal complete → 事件 [False, True] 且 event_id 不同 → 两次重放幂等”;test_todo_mutation_authority覆盖 successor 不可替换、原 turn key 必配、event-projected 升级与重放。
关键行为讲解
terminal_upgrade_requested:只在“已 done 但尚无 no_followup”时成立;普通重复完成仍走幂等重放,status_changed=false。- 两道护栏:
completion_turn_key缺失或不匹配 →ValueError("requires the original completion_turn_key");已有 successor →ValueError("cannot replace an existing successor")——升级绝不覆盖既有后续计划。 - 不可变追加:ordinary 与 terminal 是两个独立事件(
event_id不同),terminal 事件保留原completed_at、只更新updated_at与no_followup;rollout receipt 的 idempotency 含status,避免 replay 把两条事件合并。
正向路径
同 Turn:todo complete(普通,successor 已建/未置 no-follow-up)→ writeback + spend 成功 → 再次 todo complete --no-follow-up --turn-instance-id <同 key> → 通过护栏 → 追加 terminal 事件与 terminal_no_followup receipt → 投影 no_followup=true → 完成结算。
负向路径
跨 Turn 用不同 key 升级 → 拒绝且不追加事件;已有 successor 时升级 → 拒绝;升级后重放 → idempotent_replay=true、不新增事件;普通完成重放 → 仍幂等。
对主干的风险
无阻断项。 commit 53dbfbae1 含 Signed-off-by(Sign-off check pass);GitHub build/dependency-review pass,pytest 评审时刻 pending。exact head 上 test_quota_settlement_cli.py + test_todo_mutation_authority.py 48 passed,test_visible_goal_terminal_settlement.py + test_todo_completion_validation.py 22 passed,共 70 个 focused 测试覆盖本 PR 全部改动面。
非阻断(P2):确认全量 pytest 在合并前变绿(本轮 focused 70 passed);todo_event.py 的 terminal status 命名与 status 加入 idempotency 是协议字段变化,建议在 release notes 中注明事件身份语义。
验证矩阵(exact head 实测):
| 场景 | 结果 |
|---|---|
test_quota_settlement_cli.py + test_todo_mutation_authority.py |
48 passed |
test_visible_goal_terminal_settlement.py + test_todo_completion_validation.py |
22 passed |
| commit sign-off | 存在 → Sign-off check pass |
| GitHub checks | build/dependency-review pass;pytest pending |
| 改动范围 | 8 files, +269/-17,control-plane 结算/事件 |
我的整体评价
这是对同一 Turn 终态结算缺口的精准修复:升级路径受原 turn key 与 successor 双护栏约束,事件 append-only、幂等、保留原 completed_at,且 CLI 与 event-sourced 两条路径同步实现并用测试锁定顺序(ordinary → terminal → replay)。无阻断项。结论:APPROVE。
English Verdict
Verdict: APPROVE — exact head 3261@53dbfbae148965c62c8119282aa453346fe0ac8c.
Summary: A focused, well-guarded fix that lets the original completion turn append the missing terminal no-follow-up state via an append-only upgrade: it requires the original completion_turn_key, refuses to replace an existing successor, preserves completed_at, emits a distinct immutable terminal event with a terminal_no_followup rollout receipt, and stays idempotent on replay. 70 focused tests pass at the exact head (48 settlement/mutation + 22 terminal/completion); Sign-off, build, and dependency-review are green, with pytest pending at review time.
|
Self-review complete. The final diff makes Todo completion continuation an explicit typed state ( |
a42a389 to
6a5b644
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
精确评审头(Exact Head): 3261@6a5b6440c0ae373ad94294a4ad5e35e48a0fe5cc
Base: main | 类型: todo completion continuation explicit(22 files, +597/-66)
作者: huangruiteng(owner)
详细中文评审
动机
此前 #3261 的第一版只解决“same-turn 终态升级”的 append-only 路径,但完成态缺少显式的类型化续接记录:active_goal / successor / no_followup 只能从散落的 no_followup 与 successor_todo_ids 间接推断。这会让未写 no_followup=true 的完成记录与“语义 no-follow-up ACK”产生投影裂缝(与 #3264 同族)。本提交把完成续接变成持久化的一等字段,并对缺失/矛盾状态 fail-closed。
改动思路
新增 completion_state.py:用枚举固定三种续接(active_goal/successor/no_followup)与唯一恢复标记(same_turn_terminal_closeout),写出时由 completion_state_for_todo_write 统一推导并拒绝 no_followup+successor 的组合。随后把该字段接入 todo 元数据 schema、compact/完整投影、event sourcing、Markdown 行更新与 CLI 事件回读;完成重放(fence)、事件投影完成(event writeback)与持久化完成投影(durable completion)全部改为“缺失即失败、矛盾即失败”,仅保留同 completion_turn_key 内 active_goal -> no_followup 的窄恢复缝。
具体改动
loopx/control_plane/todos/completion_state.py(+149):类型化续接/恢复枚举、归一化、校验、写前推导。completion_fence.py/event_writeback.py/durable_completion.py:终态升级先校验completion_continuation=active_goal且无 successor,再校验原completion_turn_key;未键入的历史完成记录要求先修复再升级;持久化投影在require_done时缺字段即抛错。contract.py/line_update.py/summary_item.py/todo_summary.py/state_projection.py/event_sourced_state.py:新字段进入元数据 schema、Markdown round-trip、紧凑/完整投影与事件持久化。todos.py:complete_goal_todo改用completion_state_for_todo_write与linked_successors_from_state,写回completion_continuation/completion_recovery。- 文档:
loopx-turn-v0.md与turn-loop-controller-v0.md明确“缺失/矛盾完成态拒绝而非推断”,并把恢复缝限定为同一completion_turn_key内的active_goal -> no_followup。
关键内容讲解
- fail-closed 而非推断:完成记录缺
completion_continuation时,旧逻辑会按no_followup/successor/兜底推断;现在require_done投影直接抛错,并给出loopx todo complete修复指引,避免“语义 ACK 替身”掩盖 Todo 生命周期真相。 - 窄恢复缝:只有原来显式
active_goal、且仍持有原completion_turn_key、无 successor 的完成记录,才能在同一 Turn 内升级为no_followup,并留下completion_recovery=same_turn_terminal_closeout审计痕迹;不能跨 Turn、不能替换 successor。 - 契约一致性:
successor必须真有 successor、no_followup必须真无 successor、active_goal两者皆无;任何矛盾都在写入/重放/投影三层分别拦截。
对主干的风险
无阻断项。风险点是历史已完成记录缺新字段会触发 fail-closed:这正是设计意图(先修复再升级),且错误信息带明确修复命令。P2:GitHub pytest 检查在评审时刻仍 IN_PROGRESS;本地已在 exact head 跑通 156 个相关测试与投影 gap smoke,合并前确认 CI 全绿即可。
验证矩阵(exact head 实测)
| 场景 | 结果 |
|---|---|
pytest test_durable_completion_projection / quota_settlement_cli / todo_metadata_schema / todo_mutation_authority / visible_goal_terminal_settlement / host_loop_activation |
156 passed |
examples/state-projection-gap-smoke.py |
PASS |
| Sign-off / build | SUCCESS(GitHub) |
| pytest(CI) | IN_PROGRESS(评审时刻) |
我的整体评价
方向正确且收得很紧:显式类型化续接消除了推断裂缝,恢复缝被压缩到“同 Turn、同 key、无 successor”并留审计字段,文档同步更新。结论:APPROVE(author-owned PR,以 COMMENTED 记录批准结论;合并前确认 CI pytest 绿)。
English Verdict
Verdict: APPROVE (author-owned PR; recorded as COMMENTED because GitHub blocks formal self-approval). The change makes Todo completion continuation an explicit typed field, fails closed on missing or contradictory state, and keeps the same-turn terminal recovery seam narrow and audited. 156 focused tests and the state-projection gap smoke pass at the exact head; CI pytest was still in progress at review time.
6a5b644 to
2fc36ea
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
精确评审头(Exact Head): 3261@2fc36ea66028885e4919ee6d4bb10e830e60165b
Base: main | 类型: completion continuation rebase(内容与已批准头 6a5b644 相同)
作者: huangruiteng(owner)
详细中文评审
动机
作者把已批准的完成续接改动 rebase 到当前 main(内含已合并的 #3262/#3265/#3266),产生新 SHA 2fc36ea66。评审目标是确认 rebase 后内容无漂移,并核对当前头的检查状态。
改动思路
逐文件对比前一个已批准头 6a5b6440c 与本头 2fc36ea66:PR 的 22 个改动文件(completion_state.py、todos.py、completion_fence/event_writeback/durable_completion、契约/投影/Markdown round-trip、4 个测试文件与 2 个协议文档)diff 为空——内容完全一致,仅基于新 main 重建。
具体改动
与已批准的 6a5b644 内容相同,不重复展开:typed completion_continuation/completion_recovery、fail-closed 投影、同一 completion_turn_key 内的窄恢复缝。本头无新增功能改动。
关键内容讲解
- rebase 无漂移:22 个 PR 文件在两 SHA 间
git diff为空,文档/代码/测试逐字节一致。 - 检查状态:Sign-off、dependency-review、Release Artifacts build 均 SUCCESS;pytest IN_PROGRESS;Frontstage Pages(docs build)FAILURE。
- 红色检查溯源:Frontstage Pages 的失败是
rewrite_capability_links.py在 CI docs 环境ModuleNotFoundError: loopx,已确认是 main 上的既有问题——61fdf19c5(#3265 合并前)与69db2ad0e(#3265 合并提交)同一 workflow 同样失败,本 PR diff 未触及 docs 构建配置,非本改动引入。
对主干的风险
PR 内容无新增风险(与已批准头一致,本地验证全绿)。合并前必须解决 main 上既有的 Frontstage Pages docs 构建失败(该红色检查会阻塞合并),但这不属于本 PR 的改动面;建议单独修复 CI docs 构建环境(安装 loopx 包后再跑 mkdocs hook)。
验证矩阵(exact head 实测)
| 场景 | 结果 |
|---|---|
与已批准头 6a5b644 的 PR 文件 diff |
空(内容一致) |
pytest(completion/projection/settlement 相关 6 个套件) |
156 passed |
examples/state-projection-gap-smoke.py |
PASS |
| Sign-off / dependency-review / Release Artifacts build | SUCCESS(GitHub) |
| pytest(CI) | IN_PROGRESS(评审时刻) |
| Frontstage Pages(docs build) | FAILURE(main 既有问题,非本 diff 引入) |
我的整体评价
rebase 干净、内容与已批准头完全一致、本地验证全绿;唯一红色检查是 main 上既有的 docs 构建环境问题,需在合并前单独修复。结论:APPROVE(author-owned PR,以 COMMENTED 记录批准结论;合并前置条件 = CI pytest 绿 + 修复/绕过 main 既有 Frontstage Pages 失败)。
English Verdict
Verdict: APPROVE (author-owned PR; recorded as COMMENTED because GitHub blocks formal self-approval). The new head is a clean rebase: all 22 PR files are byte-identical to the previously approved head 6a5b644, 156 focused tests and the projection-gap smoke pass locally. The only failing check (Frontstage Pages docs build) is a pre-existing main issue (ModuleNotFoundError: loopx in the mkdocs hook, also failing on main commits before and after #3265) and is not introduced by this diff; it must be fixed or cleared before merge.
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangrt01 <huangrt01@163.com>
2fc36ea to
1d53fc3
Compare
|
Requalified after rebasing onto Changed surfaces reviewed: typed Todo completion continuation, same-turn terminal closeout recovery, durable/event/status/quota projections, and turn protocol docs. Validation:
Self-review confirms the recovery seam remains deliberately narrow: only the same Turn may upgrade |
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
精确评审头(Exact Head): 3261@1d53fc3085d361bea575f34f6f93797907723699
Base: main | 类型: completion continuation 再 rebase(内容与已批准头 2fc36ea 相同)
作者: huangruiteng(owner)
详细中文评审
动机
作者再次把已批准的完成续接改动 rebase 到更新后的 main(含已合并的 #3258/#3268),产生新 SHA 1d53fc30。评审目标是确认 rebase 无内容漂移,并核对当前头的检查状态。
改动思路
逐文件对比上一已批准头 2fc36ea66 与本头 1d53fc30:PR 的 22 个改动文件(completion_state.py、todos.py、completion fence/writeback/durable 投影、契约/投影/Markdown round-trip、4 个测试文件与 2 个协议文档)diff 为空——内容完全一致,仅基于含 #3258/#3268 的新 main 重建。
具体改动
与已批准头内容相同,不重复展开:typed completion_continuation/completion_recovery、fail-closed 投影、同一 completion_turn_key 内窄恢复缝。本头无新增功能改动。
关键内容讲解
- rebase 无漂移:22 个 PR 文件在两 SHA 间
git diff为空。 - 检查状态:Sign-off、dependency-review SUCCESS;build 与 pytest IN_PROGRESS;当前 check-runs 无失败(早期观测到的 build/deploy 失败来自 force-push 前的旧 run,已被新 run 取代)。
- Frontstage Pages 修复已并入:本头包含 #3268 的 docs 构建修复,此前 main 上的
ModuleNotFoundError: loopx回归已解决。
对主干的风险
无新增风险(内容与已批准头一致,本地验证全绿)。合并前确认 CI build/pytest 完成且无失败即可。
验证矩阵(exact head 实测)
| 场景 | 结果 |
|---|---|
与已批准头 2fc36ea66 的 PR 文件 diff |
空(内容一致) |
pytest(completion/projection/settlement 相关 6 个套件) |
156 passed |
examples/state-projection-gap-smoke.py |
PASS |
| Sign-off / dependency-review | SUCCESS(GitHub) |
| build / pytest(CI) | IN_PROGRESS(评审时刻;当前 run 无失败) |
我的整体评价
再 rebase 依旧干净:内容与已批准头逐字节一致,本地 156 测试与投影 smoke 全绿,当前 CI 无失败。结论:APPROVE(author-owned PR,以 COMMENTED 记录批准结论;合并前确认 CI build/pytest 绿)。
English Verdict
Verdict: APPROVE (author-owned PR; recorded as COMMENTED because GitHub blocks formal self-approval). This head is another clean rebase: all 22 PR files are byte-identical to the previously approved head 2fc36ea6, 156 focused tests and the projection-gap smoke pass locally, and current CI runs show no failures (build and pytest in progress; Sign-off and dependency-review green). The #3268 docs-build fix is now included in the base.
Motivation
A Turn can durably record an ordinary Todo completion before it reaches mandatory terminal settlement. Previously the record did not explicitly distinguish an active-goal continuation from a successor or terminal closeout, so a same-turn retry could either be replayed too early or infer lifecycle intent from the absence of fields.
What changed
active_goal,successor, orno_followup.loopx todo complete; it is never inferred asactive_goalduring read/projection.active_goaltono_followup, it cannot replace a successor, and it recordscompletion_recovery=same_turn_terminal_closeout.Validation
139 passed: metadata schema, durable completion projection, mutation authority, quota settlement CLI, and Turn controller.192 passed: event migration, quota/status/list projection, Todo lifecycle, state refresh, Turn driver/executor/transaction/settlement parity, and disposition.git diff --checkpassed.ok; no unreviewed debt, stale exception, or magnitude regression was introduced.Scope boundary
This PR does not make semantic progress observations authoritative over Todo lifecycle. Issue #3264 spans refresh-state, vision/path terminal consistency, and replan ACK clearing, and should consume this typed continuation in a separate change.