Skip to content

fix(goal): recover same-turn terminal closeout - #3261

Merged
huangruiteng merged 3 commits into
mainfrom
codex/terminal-closeout-idempotent-upgrade-20260816
Aug 16, 2026
Merged

fix(goal): recover same-turn terminal closeout#3261
huangruiteng merged 3 commits into
mainfrom
codex/terminal-closeout-idempotent-upgrade-20260816

Conversation

@huangruiteng

@huangruiteng huangruiteng commented Aug 16, 2026

Copy link
Copy Markdown
Owner

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

  • Make every new durable completion select exactly one typed continuation: active_goal, successor, or no_followup.
  • Fail closed when a completed Todo lacks the typed state. An agent can explicitly repair that row by replaying ordinary loopx todo complete; it is never inferred as active_goal during read/projection.
  • Keep the fix(goal): recover same-turn terminal closeout #3261 recovery seam narrow: only the original completion Turn may transition active_goal to no_followup, it cannot replace a successor, and it records completion_recovery=same_turn_terminal_closeout.
  • Preserve the typed state and recovery provenance across Markdown, event-sourced projection, list/status/quota summaries, rollout receipts, and durable Turn settlement.
  • Move completion-state selection into its Todo bounded context and extract successor lookup from the oversized CLI module; no maintainability budget or exception was raised.
  • Document the lifecycle invariant and terminal recovery boundary in both Turn protocols.

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.
  • Ruff, Python compilation, and git diff --check passed.
  • Pre-merge canary passed all 18 selected checks: 9 catalog canaries, 8 core-control-plane/canary-runner risk-profile smokes, and public/private boundary scan.
  • The maintainability ratchet is ok; no unreviewed debt, stale exception, or magnitude regression was introduced.
  • Failures/skips/manual holds: none in the final run.

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.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_replaycomplete_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_atno_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 升级与重放。

关键行为讲解

  1. terminal_upgrade_requested:只在“已 done 但尚无 no_followup”时成立;普通重复完成仍走幂等重放,status_changed=false
  2. 两道护栏completion_turn_key 缺失或不匹配 → ValueError("requires the original completion_turn_key");已有 successor → ValueError("cannot replace an existing successor")——升级绝不覆盖既有后续计划。
  3. 不可变追加:ordinary 与 terminal 是两个独立事件(event_id 不同),terminal 事件保留原 completed_at、只更新 updated_atno_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 53dbfbae1Signed-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 passedtest_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.

@huangruiteng

Copy link
Copy Markdown
Owner Author

Self-review complete. The final diff makes Todo completion continuation an explicit typed state (active_goal | successor | no_followup), keeps same-turn terminal closeout as an audited one-way recovery, and fails closed on untyped completed rows until the agent explicitly repairs them. I also responded to the maintainability canary by extracting the completion-state owner and existing successor lookup instead of raising budgets. Validation: 139 focused tests, 192 broader parity/projection tests, Ruff/compile/diff checks, maintainability ratchet ok, and premerge canary 18/18 with no holds. #3264 remains deliberately separate because it changes refresh-state/vision/replan transaction semantics.

@huangruiteng
huangruiteng force-pushed the codex/terminal-closeout-idempotent-upgrade-20260816 branch from a42a389 to 6a5b644 Compare August 16, 2026 15:09

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_followupsuccessor_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_keyactive_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.pycomplete_goal_todo 改用 completion_state_for_todo_writelinked_successors_from_state,写回 completion_continuation/completion_recovery
  • 文档:loopx-turn-v0.mdturn-loop-controller-v0.md 明确“缺失/矛盾完成态拒绝而非推断”,并把恢复缝限定为同一 completion_turn_key 内的 active_goal -> no_followup

关键内容讲解

  1. fail-closed 而非推断:完成记录缺 completion_continuation 时,旧逻辑会按 no_followup/successor/兜底推断;现在 require_done 投影直接抛错,并给出 loopx todo complete 修复指引,避免“语义 ACK 替身”掩盖 Todo 生命周期真相。
  2. 窄恢复缝:只有原来显式 active_goal、且仍持有原 completion_turn_key、无 successor 的完成记录,才能在同一 Turn 内升级为 no_followup,并留下 completion_recovery=same_turn_terminal_closeout 审计痕迹;不能跨 Turn、不能替换 successor。
  3. 契约一致性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.

@huangruiteng
huangruiteng force-pushed the codex/terminal-closeout-idempotent-upgrade-20260816 branch from 6a5b644 to 2fc36ea Compare August 16, 2026 16:01

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.pytodos.pycompletion_fence/event_writeback/durable_completion、契约/投影/Markdown round-trip、4 个测试文件与 2 个协议文档)diff 为空——内容完全一致,仅基于新 main 重建。

具体改动

与已批准的 6a5b644 内容相同,不重复展开:typed completion_continuation/completion_recovery、fail-closed 投影、同一 completion_turn_key 内的窄恢复缝。本头无新增功能改动。

关键内容讲解

  1. rebase 无漂移:22 个 PR 文件在两 SHA 间 git diff 为空,文档/代码/测试逐字节一致。
  2. 检查状态:Sign-off、dependency-review、Release Artifacts build 均 SUCCESS;pytest IN_PROGRESS;Frontstage Pages(docs build)FAILURE。
  3. 红色检查溯源: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>
@huangruiteng
huangruiteng force-pushed the codex/terminal-closeout-idempotent-upgrade-20260816 branch from 2fc36ea to 1d53fc3 Compare August 16, 2026 16:28
@huangruiteng

Copy link
Copy Markdown
Owner Author

Requalified after rebasing onto a04f58e81 (including the merged Frontstage fix in #3268).

Changed surfaces reviewed: typed Todo completion continuation, same-turn terminal closeout recovery, durable/event/status/quota projections, and turn protocol docs.

Validation:

  • 78 focused completion/settlement tests passed.
  • Ruff passed on all changed Python and tests.
  • Control-plane maintainability ratchet passed with no new or enlarged exception.
  • loopx canary premerge --from-git-diff: 18/18 selected checks passed, no failures, no manual holds.
  • Public/private boundary scan passed.

Self-review confirms the recovery seam remains deliberately narrow: only the same Turn may upgrade active_goal to terminal no_followup; an existing successor and cross-Turn rewrites remain rejected. The PR establishes one durable typed continuation truth rather than retaining a read-time legacy interpretation.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.pytodos.py、completion fence/writeback/durable 投影、契约/投影/Markdown round-trip、4 个测试文件与 2 个协议文档)diff 为空——内容完全一致,仅基于含 #3258/#3268 的新 main 重建。

具体改动

与已批准头内容相同,不重复展开:typed completion_continuation/completion_recovery、fail-closed 投影、同一 completion_turn_key 内窄恢复缝。本头无新增功能改动。

关键内容讲解

  1. rebase 无漂移:22 个 PR 文件在两 SHA 间 git diff 为空。
  2. 检查状态:Sign-off、dependency-review SUCCESS;build 与 pytest IN_PROGRESS;当前 check-runs 无失败(早期观测到的 build/deploy 失败来自 force-push 前的旧 run,已被新 run 取代)。
  3. 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.

@huangruiteng
huangruiteng merged commit 0cd999c into main Aug 16, 2026
8 of 10 checks passed
@huangruiteng
huangruiteng deleted the codex/terminal-closeout-idempotent-upgrade-20260816 branch August 16, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant