fix(replan): decide a lane's periodic review on lane-complete run history - #4561
Conversation
…tory An agent lane's autonomous-replan triggers count only that lane's material runs since its own last replan ACK, while the Goal-wide status window is shared with every peer lane. On a multi-lane Goal, peer volume can push the lane's own rows (and its last ACK) out of that window, so the guarded Turn selects a different replan obligation generation than the state-refresh writeback derives from the complete run index. The guarded writeback then defers silently, no ACK is ever recorded, and the lane can never discharge the obligation: every wake re-selects the same unsatisfiable replan. Keep the goal-wide display window and add the requesting lane's own AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK rows, so lane-scoped decisions see the same history the writeback derives from. The projection cache key now includes the requested lane. Validation: focused lookback/window regressions, refresh-state replan gate, quota settlement CLI, quota/status/history suites, and the live lane (codex-side-bypass) guard now selects the same obligation id the writeback derives. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head: 774e16bcca2cdfd73d85b3cfb5db248181986110
动机
一条 lane 可以卡在永远无法结算的 replan 义务上:每次唤醒都选中同一个 obligation,但 writeback 永远记不下 ACK。真实 lane codex-side-bypass(loopx-meta,13 条已注册 agent lane,25,942 条 run 记录)的证据是:受 guard 保护的 Turn 选中 replan-ccdd5e1a6f4660a4(long_todo_chain,frontier revision …cfb0…),而 state-refresh 从完整 run index 推导出 replan-d6ab0d3c5e6a1b8e(periodic_review_due);于是 enforce_open_replan_writeback(..., guard_scoped=True, guard_semantic_replan_obligation_id="replan-ccdd5e1a6f4660a4") 返回 None——typed semantic delta 被静默推迟,autonomous_replan_ack 一直是 null,下一次唤醒 guard 又选中同一个 obligation。
根因是同一个 lane 的两次推导读了不同的历史窗口:guard 走 goal 级 AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK(60 行)窗口,writeback 走完整 index。而 periodic 规则数的正是该 lane 自上次 ACK 以来的 material run(阈值 20)。在这个 goal 里 12 条 peer lane 共同填满 60 行窗口后,本 lane 自己的行(以及它上一条 ACK)掉出窗口:guard 少看见了自己的 review,就退回 todo-chain 触发。受影响的是所有多 lane Goal 上被 peer 流量稀释的 lane,以及看着这些 lane 停摆的操作者;代价是每次唤醒都消耗一个 turn 却无法推进交付 frontier——本 PR 的动机就是把这条推导收敛成同一条规则。它也回答了“#4554 是否已修”:不会,我已验证。
改动思路
入口是 loopx quota should-run / loopx status --agent-id / loopx turn decision(经 prepare_quota_command_context、handle_status_command)。权威状态是 history.collect_history 产出的 run_history.goals[].latest_runs:现在由 latest_runs_with_agent_context 在 goal 窗口之外再加入请求 lane 自己最新的 AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK 行。这样单 lane Goal 原本拥有的预算,在多 lane Goal 里也照样给到该 lane;decision owner 仍是 goal-frontier reducer 与 typed evaluator,本 PR 只让它们看到自己 lane 的完整窗口。
正向路径(真实 lane,走真实 CLI 入口):收集窗口从 60 行变为 84 行(60 goal 行 + 该 lane 自己 60 行,回看从 18:42 延伸到 17:07),把该 lane 上一条被接受的 ACK(18:02)带进视野;guard 自己推导出的 obligation 立刻变成 replan-d6ab0d3c5e6a1b8e,与 writeback 的推导完全一致,enforce_open_replan_writeback 于是记录 ACK 而不再推迟。
复用判断:没有新模块、新协议字段、新 fixture 家族。复用的是既有窗口构造器、既有的 per-lane 预算常量、既有 agent-lane lookback 模式(_status_collection_limit_for_agent_lane)以及既有 projection cache key/metadata 形状。刻意没有把窗口规则复制成第二份实现,也没有为了对齐而去改 writeback 那份 fail-closed 输入。边界写清楚:agent_lane_id 缺省时行为逐字节不变;lane 在该 goal 没有记录时也原样返回 goal 窗口。
具体改动
10 个文件、+243/-7:窗口语义本体 39 行(含 docstring)、可选 agent_lane_id 穿透 19 行、cache key 9 行、常量归属迁移 1 处、定向回归 171 行。
关键代码讲解
run_context_retention.py:265 latest_runs_with_agent_context:由runs[:limit]改为“goal 行 + 请求 lane 最新 60 条可归属行”的并集(保持 newest-first)。不变量:lane 缺席 → 原样返回;goal 行与排序不变。归因复用run_history_agent_id,不做任何 prose/子串匹配。autonomous_replan_ack.py:7 AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK:常量迁到它真正的归属模块(AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW旁边),loopx.status继续以同名同值导出,外部契约不变。status_projection_cache.py:51 status_projection_cache_key:key 增加agent_lane_id。因为 payload 现在带 lane 语义,缓存不能跨 lane 复用;缺省时归一化为null,老 key 语义不破。cli_commands/quota_context.py/status.py/turn_decision.py:四个 agent-lane 入口传入agent_lane_id=args.agent_id,其余 caller 不传——这就是“默认关闭”的边界。
对主干的风险
负向路径(未修的 base):同样 lane、同样 delta,guard 选中 replan-ccdd5e1a6f4660a4,writeback 推导 replan-d6ab0d3c5e6a1b8e → enforce_open_replan_writeback 静默 defer:不记 ACK、不报错、义务跨唤醒存续(turn 2026-09-16T13:10:29.000Z 的 receipt 就是现场)。若把 lane 窗口关掉,两条新回归立刻失败,说明它们精确钉住目标行为。
验证:test_autonomous_replan_periodic_lookback.py 6 passed(含 mutation 验证);replan/quota 控制面 79 passed;status/history/turn 面 322+19 passed;quota 投影与 frontier 规则 74 passed。canary premerge 18 项里 direct checks、python_compile、risk-profile smokes 8/8 通过;两个 catalog 失败都可归因:control-plane-maintainability-ratchet-smoke 只报 loopx/chat_runtime.py(本 PR 未改,未修 base 上同样失败,属继承问题);semantic-vocabulary-drift-smoke 在本 worktree 缺 node_modules(TS parser)时失败,链接后通过。test_quota_settlement_cli.py 的 2 条 [*-sqlite] 失败是本机既有环境缺口(Node 25.5.0 / SQLite 3.51.2,需 Node 22.22.3),未修 base 上同样失败;没有任何失败指向被改文件。
语义与 CI 对齐
periodic 规则的语义是“该 lane 自上次被接受 ACK 以来有 ≥20 条 material run”。改动后 guard 与 writeback 在同一条 lane 行集合上评估这句陈述,因此 obligation id 一致;我没有放宽 guard-scope 的精确 id 比较,也没有把机器强制义务降级为“建议”——恰恰相反,是让 guard 自己选中的义务变成可结算的。risk:窗口预算仍是有界的 per-lane 预算,若某 lane 在没有任何被接受 ACK 的情况下产出超过该预算的行,仍无法只靠 status payload 判定;这是既有的单 lane 假设,现在至少在 lane 之间一致。authority:没有新授权,没有新 actor/生命周期,public 名字(agent_lane_id)只 scope 它声明的东西。default-off 一致性由“不传 lane 即完全不激活 + 既有默认路径套件全通过”证明。
我的整体评价
APPROVE。 这是一处“把已有规则收回它的边界”的修复:规则仍是 period review 的 20 条 material run,owner 仍在 typed 侧,guard 仍是该 Turn 的权威;改变的只是让 guard 看见自己 lane 的窗口,从而和 writeback 得到同一代 obligation,让原本选中的义务第一次变得可以结算。证据链完整:真实 lane 上 before/after 的 obligation id、guard-scope 下 delta 从“静默 defer”变为“记录 ACK”、dry-run refresh-state 报 autonomous_replan_recorded=true,以及关闭窗口即失败的 mutation 回归。需要提前说明的边界:本 lane 当前的运行时仍是修复前的 release 快照,它要恢复还需要运行时升级到包含本改动(外加一次干净的 guard 运行清掉在途 receipt)。
English verdict: APPROVE — at head 774e16b this makes the guarded Turn derive a lane's replan obligation from that lane's own newest material rows instead of a goal-wide window peer lanes also fill, so the guard and the refresh-state writeback agree on one obligation generation and a typed semantic delta can finally be recorded as an ACK (live evidence: ccdd5e1a6f4660a4 vs d6ab0d3c5e6a1b8e before, one shared id after; the lane window disabled makes the new regression fail). The change is opt-in per request, reuses the existing window builder and the existing per-lane budget, keeps the exported constant and the default collection path byte-identical, and moves no authority: the guard stays the turn authority and the writeback still requires a typed semantic delta. Validation: the new multi-lane regressions, 79 replan/quota control-plane cases, 341 status/history/turn cases, 74 quota/frontier cases, and the live before/after probe; the only red items are the host SQLite qualification gap and an inherited ratchet finding that reproduce identically on the unmodified base. #4554 does not fix this deadlock: with it merged, the guard still selected ccdd5e1a6f4660a4 while the writeback derived d6ab0d3c5e6a1b8e.
Self-merge record (author-owned; admin bypass)Exact head reviewed and merged: Changed surfaces. Checks run.
Failures and skips (all inherited, none on a changed file).
Manual holds.
|
Problem
A lane can be stuck in an unsatisfiable autonomous-replan gate: every wake selects a replan obligation whose ACK can never be recorded.
Live evidence from the
codex-side-bypasslane in theloopx-metaGoal (13 registered agent lanes, 25.9k run records):replan-ccdd5e1a6f4660a4(long_todo_chain, frontier revision…cfb0…);replan-d6ab0d3c5e6a1b8e(periodic_review_due) from the complete run index;enforce_open_replan_writeback(..., guard_scoped=True, guard_semantic_replan_obligation_id="replan-ccdd5e1a6f4660a4")therefore returnedNone; the typed semantic delta was silently deferred,autonomous_replan_ackstayednull, and the guard re-selected the same obligation on the next wake.The two derivations disagreed because they read different histories of the same lane:
AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK(60) rowsThe lane's material runs since its last replan ACK are what the periodic trigger counts (
AUTONOMOUS_REPLAN_PERIODIC_RUN_THRESHOLD = 20). With 12 peer lanes sharing the goal-wide window, the lane's own rows and its last ACK fall outside that window, so the guard under-observed its own review and fell back to the todo-chain trigger.Change
Keep the goal-wide display window and add the requesting lane's own
AUTONOMOUS_REPLAN_PERIODIC_LOOKBACKrows to the collected history. A lane now gets the same decision budget a single-lane Goal would have, so lane-scoped replan triggers are decided on the same history the writeback derives from.AUTONOMOUS_REPLAN_PERIODIC_LOOKBACKmoves to the replan-ack owner module (the window it belongs to) and stays exported fromloopx.status. The status projection cache key includes the requested lane, since the payload is now lane-scoped.Validation
tests/control_plane/test_refresh_state_replan_gate.py,test_replan_settlement_runtime.py,test_replan_successor_durable_ack.py,test_scheduler_ack_current_host_binding.py,test_todo_decision_scope_lifecycle.py,test_goal_acceptance_observation.py: 79 passed;tests/test_status_server_fast_path.py,test_loopx_turn_driver.py,test_summary_all.py,test_global_risks.py,test_cli_argument_diagnostics.py,test_quota_settlement.py: 322 passed;test_quota_should_run_parity.py,test_quota_recent_runs.py,test_quota_cli_projection.py,test_quota_run_decision.py,test_goal_frontier_replan_rules.py,test_replan_host_context_projection.py,test_monitor_replan_agent_scope.py: 74 passed;test_quota_settlement_cli.py+test_effect_turn_live_quota_decision.py: 75 passed, 2 failed — both are the pre-existing[*-sqlite]arms that fail on this host (SQLite authority runtime is not qualified (SQLite 3.51.2…)); they fail identically on the unmodified base;quota should-run --agent-id codex-side-bypassnow selectsreplan-d6ab0d3c5e6a1b8e, i.e. the same generation the writeback derives, and the guarded writeback records the ACK instead of deferring.loopx canary premerge --from-git-diffexecuted before merge.Not included: the lane's own runtime still runs the previous release snapshot, so the live lane recovers once its runtime is upgraded to this change (or any release containing it).