Skip to content

fix(replan): decide a lane's periodic review on lane-complete run history - #4561

Merged
huangruiteng merged 1 commit into
mainfrom
codex/fix-lane-periodic-window-20260916
Sep 16, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/fix-lane-periodic-window-20260916

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

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-bypass lane in the loopx-meta Goal (13 registered agent lanes, 25.9k run records):

  • the guarded Turn selected replan-ccdd5e1a6f4660a4 (long_todo_chain, frontier revision …cfb0…);
  • the state-refresh writeback derived 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 returned None; the typed semantic delta was silently deferred, autonomous_replan_ack stayed null, 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:

input guarded Turn writeback
run history goal-wide AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK (60) rows complete run index
lane rows visible 36 of 60 all

The 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_LOOKBACK rows 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_LOOKBACK moves to the replan-ack owner module (the window it belongs to) and stays exported from loopx.status. The status projection cache key includes the requested lane, since the payload is now lane-scoped.

Validation

  • new regressions: peer-lane volume cannot hide a lane's periodic review, and the lane window preserves the goal-wide rows (verified failing with the lane window disabled);
  • 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;
  • live lane: quota should-run --agent-id codex-side-bypass now selects replan-d6ab0d3c5e6a1b8e, i.e. the same generation the writeback derives, and the guarded writeback records the ACK instead of deferring.

loopx canary premerge --from-git-diff executed 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).

…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 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: 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 行。

关键代码讲解

  1. run_context_retention.py:265 latest_runs_with_agent_context:由 runs[:limit] 改为“goal 行 + 请求 lane 最新 60 条可归属行”的并集(保持 newest-first)。不变量:lane 缺席 → 原样返回;goal 行与排序不变。归因复用 run_history_agent_id,不做任何 prose/子串匹配。
  2. autonomous_replan_ack.py:7 AUTONOMOUS_REPLAN_PERIODIC_LOOKBACK:常量迁到它真正的归属模块(AUTONOMOUS_REPLAN_ACK_MATERIAL_RUN_WINDOW 旁边),loopx.status 继续以同名同值导出,外部契约不变。
  3. status_projection_cache.py:51 status_projection_cache_key:key 增加 agent_lane_id。因为 payload 现在带 lane 语义,缓存不能跨 lane 复用;缺省时归一化为 null,老 key 语义不破。
  4. 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.

@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Self-merge record (author-owned; admin bypass)

Exact head reviewed and merged: 774e16bcca2cdfd73d85b3cfb5db248181986110 (review card: #4561 (review)). Owner asked for the lane's replan deadlock to be fixed; this is the fix. No formal approval exists because the account is the author.

Changed surfaces. loopx/control_plane/runtime/run_context_retention.py (lane-complete window), loopx/history.py + loopx/control_plane/status/collection.py + loopx/status.py (optional agent_lane_id plumbing), loopx/control_plane/work_items/autonomous_replan_ack.py (lookback constant moves to its owner module, re-exported unchanged), loopx/control_plane/runtime/status_projection_cache.py (key/metadata gain the lane), loopx/cli_commands/{quota_context,status,turn_decision}.py (pass the requesting lane), tests/control_plane/test_autonomous_replan_periodic_lookback.py (new regressions).

Checks run.

  • pytest tests/control_plane/test_autonomous_replan_periodic_lookback.py 6 passed, including a mutation check (the two new behaviour cases fail with the lane window disabled).
  • pytest batches: replan/quota control plane 79 passed; status/history/turn/risk surfaces 341 passed; quota projection + frontier rules 74 passed.
  • pytest tests/control_plane/test_quota_settlement_cli.py tests/control_plane/test_effect_turn_live_quota_decision.py 75 passed, 2 failed.
  • loopx canary premerge --from-git-diff: direct checks and python_compile passed, risk-profile smokes 8/8 passed, 18 selected.
  • Live-lane before/after probe on the real runtime (25.9k run records) and through the real CLI entrypoint.

Failures and skips (all inherited, none on a changed file).

  • [*-sqlite] quota-settlement arms fail on this host before any assertion: SQLite authority runtime is not qualified (SQLite 3.51.2, synchronous statement finalization=true); require Node 22.22.3 / SQLite 3.51.3. The same two cases fail on the unmodified base.
  • control-plane-maintainability-ratchet-smoke / tests/canary/test_maintainability_ratchet.py report only module_metric_budget:loopx/chat_runtime.py, a file this PR does not touch; identical output on the unmodified base.
  • tests/cli_commands/test_project_lifecycle_goal_channel.py fails 9 cases on this CI run (loopx.cli_commands.project_lifecycle has no attribute refresh_state_run) and fails identically on the unmodified base — pre-existing repo breakage, not this diff.
  • dashboard-acceptance fails with “Chat runtime picker ignored the declared steward executor: Chat Codex”, a dashboard/steward surface this PR does not touch.
  • semantic-vocabulary-drift-smoke initially failed in a fresh worktree because its TypeScript parser needs node_modules; it passes once node_modules is linked.

Manual holds.

  • loopx pr-review --check-merge-readiness 4561@<head> cannot return ready=true while the inherited CI failures above are red on main; the merge is therefore an owner-requested admin bypass, not a green-gate merge. No failing check names a file this PR changes.
  • The lane's live runtime still runs a release snapshot without this change. The lane recovers on its next runtime update, plus one clean guard run to retire the in-flight receipt that still names the previous obligation generation.

@huangruiteng
huangruiteng merged commit 324760f into main Sep 16, 2026
18 of 26 checks passed
@huangruiteng
huangruiteng deleted the codex/fix-lane-periodic-window-20260916 branch September 16, 2026 14:36
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