Skip to content

fix(replan): keep checkpoint replan ACKs visible past the run window - #4554

Merged
huangruiteng merged 1 commit into
mainfrom
codex/replan-checkpoint-ack-visibility-20260916
Sep 16, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/replan-checkpoint-ack-visibility-20260916

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

问题

长 Todo 链(long_todo_chain)规则是边沿触发的:一条被接受的 replan ACK 在其记录的 Todo frontier revision 与当前 revision 一致时压制该规则,只有 frontier 发生实质变化才重新武装(#3492 引入,tests/control_plane/test_goal_frontier_replan_rules.py::test_long_todo_chain_checkpoint_is_edge_triggered_and_rearms_on_change 就是这条契约)。

但 ACK 投影函数 latest_autonomous_replan_ack_for_projection 在数到 AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW(20)条 material run 后就直接 return None。于是哪怕 ACK 携带的 checkpoint 仍然精确等于当前 frontier revision,它也会因为运行次数到期而被丢掉,frontier 侧拿不到 ACK → 在没有任何 frontier 变化的情况下重新发出 long_todo_chain obligation。

后果有三层:

  1. 规则退化成"每 20 条 material run 重发一次",与独立存在的 periodic review 规则(同样的 20 条阈值、trigger kind 是 periodic_review_due)重复,而且先触发的 long-chain 分支会把后者盖掉;
  2. 重新发出的 obligation 文案仍然说"agent lane 的 todo 链过长",但 frontier 可能完全没有变化,属于误导性的根因描述;
  3. 于是"超出阈值不必反复要求 replan,而是有间隔"这个已修复的行为,只在 20 条 material run 之内成立。

证据

  • 代码:loopx/control_plane/work_items/autonomous_replan_ack.py 的窗口返回值,与 loopx/control_plane/todos/frontier_revision.ts::classifyAck 的 revision 精确匹配契约冲突;仓库里没有任何测试固定"ACK 因运行次数到期而失效"这一行为。
  • 真实数据(本机一个 agent lane 的 run history,只读):最新一条被接受的 ACK 之后有 22 条 material run,latest_autonomous_replan_ack_for_projection 返回 None;用本 PR 的改动重跑同一份 run 列表,返回的是那条 ACK,且它带着 long_todo_chain checkpoint。
  • 历史痕迹:同一 lane 上出现过一个 frontier revision(1a40ad2c…)在相隔约 12 小时的两条 ACK 里完全相同,但 obligation id 不同(replan-ccfb45db… → replan-b4bc1556…)——说明那次重发不是 frontier 变化引起的。

改动

  • loopx/control_plane/work_items/autonomous_replan_ack.py:新增 ack_binds_trigger_checkpoints(),区分"带 typed trigger checkpoint 的 ACK"和旧格式 ACK。带 checkpoint 的 ACK 在整段被扫描的 history 内保持可见;旧格式 ACK 继续使用原有 material-run 窗口;periodic review 规则本身继续按自己的窗口工作。

安全性依据:带 checkpoint 的 ACK 并不具备"无条件压制"能力——typed evaluator 只在它写明的 revision 与当前 frontier revision 完全一致时才压制,而清除既有 obligation 的路径还额外要求 obligation id 精确相等(obligation id 随 revision 轮换)。因此让旧 ACK 继续可见不会掩盖任何实质变化,只是不再让运行次数冒充实质变化。

验证

  • pytest -q tests/control_plane/test_autonomous_replan_ack.py tests/control_plane/test_goal_frontier_replan_rules.py tests/control_plane/test_canonical_frontier_revision.py:52 passed
  • pytest -q tests/control_plane -k replan:259 passed,1 failed(test_todo_replan_cadence.py::test_cli_cadence_roundtrip_syncs_to_an_isolated_runtime,本机 subprocess 取到 Python 3.9 而报 "Python 3.11+ is required",在未应用本改动的 origin/main 上同样失败,属于本机环境问题)
  • node --experimental-strip-types --test tests/control_plane_ts/frontier_revision.test.ts:8 pass
  • python examples/autonomous-replan-obligation-smoke.py:ok
  • 反向验证:把生产改动临时移除后,新增用例 test_checkpoint_ack_stays_visible_beyond_the_material_run_window 失败(返回 None),旧格式 ACK 的窗口用例仍通过,说明修复精确落在目标行为上。

English summary

The long Todo-chain rule is edge-triggered: an accepted replan ACK suppresses the rule while its recorded Todo frontier revision matches, and the rule re-arms only after a material revision. latest_autonomous_replan_ack_for_projection nevertheless stopped scanning after AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW (20) material runs, so a checkpoint-bearing ACK was dropped while its checkpoint still described the current frontier, and frontier consumers re-issued the long-chain obligation for an unchanged state. That duplicated the separate periodic review cadence and mislabeled a run-count re-arm as a long chain.

This change keeps a checkpoint-bearing ACK visible for the whole scanned history while legacy ACKs without checkpoints keep the material-run window. It is safe because checkpoint ACKs are revision-gated by the typed evaluator and the obligation-clearing path also requires an exact obligation id, so an old ACK cannot hide a materially changed frontier. Validation: 52 focused pytest cases, 8 TypeScript frontier cases, the autonomous-replan-obligation smoke, and a mutation check that fails the new regression without the production change. Evidence from a live lane: 22 material runs after the newest accepted ACK made the projection return None; the same revision was acknowledged twice under different obligation ids about 12 hours apart.

The long Todo-chain rule is edge-triggered: an accepted ACK suppresses the
obligation while its exact Todo frontier revision matches, and the rule re-arms
only after a material revision. The ACK projection stopped scanning after
AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW (20) material runs, so a
checkpoint-bearing ACK was dropped while its checkpoint still described the
current frontier. Frontier consumers that read that projection then saw no ACK
and re-issued the long-chain obligation for an unchanged frontier, duplicating
the periodic review cadence and reporting a "long chain" trigger that no longer
matched the state.

A checkpoint-bearing ACK is revision-gated by the typed evaluator, and the
obligation-clearing path also requires the exact obligation id, so an old ACK
cannot suppress or clear a materially changed frontier. Keep such an ACK
visible for the whole scanned history, and keep the material-run window for
legacy ACKs without checkpoints, which is also the window the separate periodic
review rule owns.

Observed on the loopx-meta review lane: 22 material runs after the newest
accepted ACK made latest_autonomous_replan_ack_for_projection return None even
though the ACK carried a long_todo_chain checkpoint, and the same frontier
revision was acknowledged 12 hours apart under two different obligation ids.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator 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)

Reviewed exact head: 329fa5232f270742e719ad59b59a5502beb81979

动机

长 Todo 链规则是边沿触发的:被接受的 ACK 在其记录的 frontier revision 与当前 revision 一致时压制该规则,只有 frontier 发生实质变化才重新武装(#3492 引入,test_long_todo_chain_checkpoint_is_edge_triggered_and_rearms_on_change 就是这条契约)。但 ACK 投影 latest_autonomous_replan_ack_for_projection 在数到 20 条 material run 后无条件 return None,把带 checkpoint 的 ACK 也一起丢掉:于是即使 revision 一字未变,frontier 侧也拿不到 ACK,规则被"运行次数"冒充成实质变化重新触发,并且把独立存在的 periodic review 规则(同样 20 条阈值)盖掉、把根因写成"链过长"。

改动思路

判断留在 typed 侧,投影只负责"给不给候选 ACK":新增 ack_binds_trigger_checkpoints() 区分带 typed trigger checkpoint 的 ACK 与旧格式 ACK;带 checkpoint 的 ACK 在整段被扫描的 history 内保持可见,旧格式 ACK 继续走原来的 material-run 窗口,periodic review 规则继续按自己的窗口工作。这个方向之所以安全,是因为带 checkpoint 的 ACK 并不具备"无条件压制"的能力——classifyAck 只在 revision 精确相等时压制,清除既有 obligation 的路径还额外要求 obligation id 精确相等(而 obligation id 随 revision 轮换)。

具体改动

  • loopx/control_plane/work_items/autonomous_replan_ack.py(+39/-3):新增 ack_binds_trigger_checkpoints(:11);latest_autonomous_replan_ack_for_projection(:298)引入 window_expired,窗口到期后只对带 checkpoint 的 ACK 继续放行,其余仍返回 None。
  • tests/control_plane/test_autonomous_replan_ack.py(+79):test_checkpoint_ack_stays_visible_beyond_the_material_run_window(:74)与 test_legacy_ack_still_expires_with_the_material_run_window(:89),后者防住"把窗口一刀切掉"的过度修复。

关键代码讲解

  1. autonomous_replan_ack.py:11 ack_binds_trigger_checkpoints:只认 semantic_delta.trigger_checkpoints 里同时具备 kind 与 frontier_revision 的条目;畸形或旧格式 ACK 会被判为"无 checkpoint",保持旧窗口(fail-closed 方向)。
  2. autonomous_replan_ack.py:298 的循环:window_expired 一旦置位,后续只在遇到带 checkpoint 的 ACK 时才返回;不含 checkpoint 的 ACK 到了窗口仍然返回 None——这正是原行为,也是本次修复的边界。
  3. test_...:74:构造"ACK 之前已有 2 倍窗口的 material run",断言仍能拿到带 long_todo_chain checkpoint 的 ACK。把生产改动临时移除后这条会失败(我做了 mutation 验证),说明它精确钉住目标行为。
  4. test_...:89:同一位置放一条无 checkpoint 的 ACK,断言窗口到期后返回 None,与旧行为一致。

对主干的风险

  • 安全边界:让旧 ACK 继续可见,理论上可能压制"其实已经变了"的 frontier。这里由两层挡住:frontier digest 覆盖每条可选 advancement 行的身份与状态字段(updated_at 之类不影响 digest),typed evaluator 要求 revision 精确相等;清除既有 obligation 还需要 obligation id 精确相等。
  • 节奏:periodic review 规则不被这次改动触及,仍然是每 20 条 material run 的独立节奏;因此"有间隔地重规划"这条设计没有丢。

一个必须说清楚的边界(与本 PR 的定位相关):这个修复不会自动关掉一条已经打开、且 revision 已经变化的 obligation。本 lane 现在挂着的 replan-ccdd5e1a6f4660a4 记录的 revision 是 cfb03ae5…,而最后一条被接受的 ACK 记录的是 6c0af01c…(中间我改过一条 todo 文本,frontier 确实变了)。所以合并它之后,规则仍会(正确地)要求一次新的语义增量;它修的是"之后不该再因为 20 条运行次数而重发",而不是"当前这条立刻消失"。

我的整体评价

APPROVE。 这是把一个真实的边沿触发契约回收成它本来的样子:判据仍在 typed 侧,Python 只调整候选 ACK 的可见性,旧格式 ACK 与 periodic 节奏都不受影响,正反两个用例 + mutation 验证都到位。证据:pytest 三个模块 52 passed;node --test tests/control_plane_ts/frontier_revision.test.ts 8 passed;examples/autonomous-replan-obligation-smoke.py ok;对真实 run history 的 before/after 探针显示:改动前返回 None,改动后返回那条带 long_todo_chain checkpoint 的 ACK;git merge-tree 对 main 干净。

English verdict: APPROVE — at head 329fa52 this restores the documented edge trigger: a checkpoint-bearing replan ACK stays visible past the material-run window while legacy ACKs keep expiring, so the long-chain rule is no longer re-armed by run count alone and the separate periodic review cadence keeps its own window. Safety rests on the typed evaluator's exact-revision match and the exact-obligation-id requirement for clearing, both unchanged. Evidence: 52 focused pytest cases, 8 TypeScript frontier cases, the autonomous-replan-obligation smoke, a live before/after projection probe (None before, checkpoint ACK after), and a mutation check that fails the new regression without the production change. One boundary worth stating: this does not clear an already-open obligation whose recorded revision has moved — that still needs a fresh semantic delta.

@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Self-merge record (author-owned PR; owner authorized this merge in the current session).

Changed surfaces

  • loopx/control_plane/work_items/autonomous_replan_ack.py: read-projection policy for which replan ACK candidate is offered (checkpoint-bearing ACKs stay visible past AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW; legacy ACKs keep the window). Typed suppression authority (frontier_revision.ts:classifyAck) and obligation identity are unchanged.
  • tests/control_plane/test_autonomous_replan_ack.py: regression plus a legacy-window counterexample.

Checks run

  • pytest -q tests/control_plane/test_autonomous_replan_ack.py tests/control_plane/test_goal_frontier_replan_rules.py tests/control_plane/test_canonical_frontier_revision.py — 52 passed
  • node --experimental-strip-types --test tests/control_plane_ts/frontier_revision.test.ts — 8 passed
  • python examples/autonomous-replan-obligation-smoke.py — ok
  • loopx canary premerge --from-git-diff — risk-profile smokes 8/8 passed (semantic-vocabulary drift, autonomous-replan obligation, monitor-poll writeback, refresh-state write correctness, plus the canary suite)
  • Mutation check: reverting the production change makes the new checkpoint case fail while the legacy-window case still passes
  • Live before/after probe over one agent lane's real run history: None before the change, the checkpoint-bearing ACK after

Failures and skips

  • Advisory (does not mention changed files, present on the baseline): python3 examples/control_plane/control-plane-maintainability-ratchet-smoke.py fails as an inherited baseline failure.
  • Not executed: SQLite/PostgreSQL provider arms, because this host's Node 25.5.0 / SQLite 3.51.2 is rejected by the runtime-qualification check before reaching any assertion; unrelated to this diff.
  • CI status checks were intentionally not polled (wait_for_ci=false).

Manual holds

  • None. This does not clear an already-open replan obligation whose recorded frontier revision has moved; that still requires a fresh typed semantic delta.

Why the coverage is enough

The change is one predicate plus one flag in an existing projection; its failure modes are "an old ACK suppresses a changed frontier" (blocked by the typed evaluator's exact-revision match and by the exact-obligation-id clearing rule) and "legacy ACKs silently gain extended visibility" (pinned by the counterexample test). The focused suites cover both sides, and neither the writer path nor obligation identity is touched.

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