From 129bb1fe74df502bd4e5d07babcd9a58a2b34e74 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:05:26 +0800 Subject: [PATCH 1/2] fix(replan): close lane-owned long-chain obligations from projected vision Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../goals/goal_frontier/__init__.py | 20 +-- .../goals/goal_frontier/ack_policy.py | 3 +- .../goals/goal_frontier/long_todo_chain.py | 1 + .../control_plane/todos/frontier_revision.ts | 35 ++++-- .../autonomous_replan_obligation.py | 5 +- .../work_items/replan_semantics.ts | 11 +- .../test_canonical_frontier_revision.py | 3 +- .../test_goal_frontier_replan_rules.py | 2 + .../test_long_chain_projected_closeout.py | 114 ++++++++++++++++++ .../test_quota_settlement_cli.py | 97 --------------- .../frontier_revision.test.ts | 22 +++- .../control_plane_ts/replan_semantics.test.ts | 29 +++++ 12 files changed, 210 insertions(+), 132 deletions(-) create mode 100644 tests/control_plane/test_long_chain_projected_closeout.py diff --git a/loopx/control_plane/goals/goal_frontier/__init__.py b/loopx/control_plane/goals/goal_frontier/__init__.py index 610894e389..7b95fcb645 100644 --- a/loopx/control_plane/goals/goal_frontier/__init__.py +++ b/loopx/control_plane/goals/goal_frontier/__init__.py @@ -1216,7 +1216,7 @@ def derive_goal_frontier_replan_obligation_from_summaries( "kind": LONG_TODO_CHAIN_TRIGGER, "section": "agent_todo_summary", "text": ( - "current agent lane has a long selectable todo chain; " + "current agent lane has a long claimed todo chain; " "run a vision checkpoint/replan before continuing linearly" ), **long_chain_trigger, @@ -1229,18 +1229,7 @@ def derive_goal_frontier_replan_obligation_from_summaries( "update_agent_vision", "create_successor", ], - todo_actions=[ - { - "action": "add", - "role": "agent", - "priority": "P1", - "text": ( - "run a bounded long-chain vision replan: compare evidence " - "with the active vision, group or prune the todo chain, " - "and select the next high-value runnable slice" - ), - } - ], + todo_actions=[], stop_condition=( "stop if pruning or external research requires private material, " "credentials, destructive git, production actions, or owner-only decisions" @@ -1248,7 +1237,10 @@ def derive_goal_frontier_replan_obligation_from_summaries( recommended_action=( "run a bounded long-chain vision replan before continuing a 15+ " "todo lane: read evidence, use public research if local evidence " - "is weak, group/prune work, and write a concrete todo or vision delta" + "is weak, group/prune work, and record an evidence-linked vision " + "path through replan_action_packet.writeback_contract using the " + "current Turn settlement binding; retain existing runnable work " + "when appropriate instead of adding a Todo whose only action is replan" ), rearmed_after_obligation_id=( long_chain_ack_decision.rearmed_after_obligation_id diff --git a/loopx/control_plane/goals/goal_frontier/ack_policy.py b/loopx/control_plane/goals/goal_frontier/ack_policy.py index 19f372d15e..85a96286be 100644 --- a/loopx/control_plane/goals/goal_frontier/ack_policy.py +++ b/loopx/control_plane/goals/goal_frontier/ack_policy.py @@ -135,7 +135,8 @@ def replan_successor_transition_ack( eligible_ids.add(binding["todo_id"]) elif binding["kind"] == "predecessor": prior = ensure_replan_novelty_policy({**(replan_obligation or {}), "triggers": [ - {**trigger, "frontier_revision": binding["frontier_revision"]} + {**trigger, "frontier_revision": binding["frontier_revision"], + "obligation_identity_revision": binding["obligation_identity_revision"]} for trigger in (replan_obligation or {}).get("triggers") or []]}) if prior["obligation_id"] == origins[binding["todo_id"]]: eligible_ids.add(binding["todo_id"]) diff --git a/loopx/control_plane/goals/goal_frontier/long_todo_chain.py b/loopx/control_plane/goals/goal_frontier/long_todo_chain.py index 99a2e5ce31..d7dbb3d491 100644 --- a/loopx/control_plane/goals/goal_frontier/long_todo_chain.py +++ b/loopx/control_plane/goals/goal_frontier/long_todo_chain.py @@ -24,6 +24,7 @@ class LongTodoChainObservation: count_kind: str selectable_open_count: int selectable_advancement_count: int + current_agent_claimed_open_count: int current_agent_claimed_advancement_count: int unclaimed_advancement_count: int threshold: int diff --git a/loopx/control_plane/todos/frontier_revision.ts b/loopx/control_plane/todos/frontier_revision.ts index cfc225eda3..d8d7a0f37b 100644 --- a/loopx/control_plane/todos/frontier_revision.ts +++ b/loopx/control_plane/todos/frontier_revision.ts @@ -24,8 +24,10 @@ type Row = { }; type LongChainObservation = { trigger_count: number; - count_kind: "selectable_advancement_todos" | "selectable_open_todos"; + count_kind: "selectable_advancement_todos" | "selectable_open_todos" | + "claimed_advancement_todos" | "claimed_open_todos"; selectable_open_count: number; selectable_advancement_count: number; + current_agent_claimed_open_count: number; current_agent_claimed_advancement_count: number; unclaimed_advancement_count: number; threshold: 15 | 20; agent_id: string | null; frontier_revision: string | null; frontier_revision_complete: boolean; @@ -33,7 +35,7 @@ type LongChainObservation = { }; type AckDecision = {acknowledged: boolean; rearmed_after_obligation_id: string | null}; type SuccessorBinding = {kind: "exact"; todo_id: string} | - {kind: "predecessor"; todo_id: string; frontier_revision: string}; + {kind: "predecessor"; todo_id: string; frontier_revision: string; obligation_identity_revision: string}; type TriggerCheckpoint = { kind: string; frontier_revision: string; frontier_owned_identity?: string; }; @@ -162,8 +164,10 @@ function successorCheckpoints(request: JsonObject, agent: string | null): JsonOb // A new successor changes the revision it was created to settle. Reconstruct // only a unique fresh insertion, using a complete source matching the index. // The existing obligation-id owner still verifies the predecessor revision. - const priorAdvancement = count(trigger.selectable_advancement_count) - 1; - const priorOpen = count(trigger.selectable_open_count) - 1; + const priorAdvancement = count(agent === null ? trigger.selectable_advancement_count + : trigger.current_agent_claimed_advancement_count) - 1; + const priorOpen = count(agent === null ? trigger.selectable_open_count + : trigger.current_agent_claimed_open_count) - 1; if (bindings.length === 0 && candidates.length === 1 && triggers.length === 1 && trigger.kind === TRIGGER && trigger.frontier_revision === source.frontier_revision && (priorAdvancement >= 15 || priorOpen >= 20 && priorAdvancement > 0)) { const completeSource = indexed === null ? source : checkpoint(rows, agent); @@ -171,7 +175,8 @@ function successorCheckpoints(request: JsonObject, agent: string | null): JsonOb const candidate = candidates[0]; const prior = checkpoint(rows === null ? null : rows.filter(row => row.id !== candidate.todo_id), agent); if (prior.complete && prior.frontier_revision !== source.frontier_revision) { - bindings.push({kind: "predecessor", todo_id: text(candidate.todo_id), frontier_revision: prior.frontier_revision}); + bindings.push({kind: "predecessor", todo_id: text(candidate.todo_id), frontier_revision: prior.frontier_revision, + obligation_identity_revision: prior.frontier_owned_identity ?? prior.frontier_revision}); } } } @@ -229,19 +234,27 @@ export function evaluateLongTodoChain(value: unknown): JsonObject { if (request.schema_version !== "long_todo_chain_request_v0") throw new EffectRuntimeRequestError("long chain schema mismatch"); if (request.operation !== "observe") throw new EffectRuntimeRequestError("unsupported long chain operation"); const summary = object(request.summary), frontier = object(request.frontier_counts); + const agent = agentId(request.agent_id); const current = count(frontier.current_agent_claimed_advancement_count); const unclaimed = count(frontier.unclaimed_advancement_count); const advancement = current + unclaimed; const open = Math.max(advancement, request.summary == null ? count(object(request.agent_counts).open) : count(summary.current_agent_claimed_open_count) + count(summary.unclaimed_open_count)); - const threshold = advancement >= 15 ? 15 : open >= 20 && advancement > 0 ? 20 : null; + const claimedOpen = Math.max(current, count(summary.current_agent_claimed_open_count)); + // A lane replans commitments it owns. Shared candidates remain selectable, + // but must not impose a chain obligation with no owned ACK fence. + const measuredAdvancement = agent === null ? advancement : current; + const measuredOpen = agent === null ? open : claimedOpen; + const threshold = measuredAdvancement >= 15 ? 15 : measuredOpen >= 20 && measuredAdvancement > 0 ? 20 : null; if (threshold === null) return {observation: null, decision: null}; - const agent = agentId(request.agent_id); const revision = readIndex(summary.advancement_frontier_revision_index, agent) ?? checkpoint(decodeRows(request.rows), agent); - const observation: LongChainObservation = {trigger_count: threshold === 15 ? advancement : open, - count_kind: threshold === 15 ? "selectable_advancement_todos" : "selectable_open_todos", + const observation: LongChainObservation = {trigger_count: threshold === 15 ? measuredAdvancement : measuredOpen, + count_kind: agent === null + ? threshold === 15 ? "selectable_advancement_todos" : "selectable_open_todos" + : threshold === 15 ? "claimed_advancement_todos" : "claimed_open_todos", selectable_open_count: open, selectable_advancement_count: advancement, + current_agent_claimed_open_count: claimedOpen, current_agent_claimed_advancement_count: current, unclaimed_advancement_count: unclaimed, threshold, agent_id: agent, frontier_revision: revision.complete ? revision.frontier_revision : null, frontier_revision_complete: revision.complete, @@ -249,6 +262,8 @@ export function evaluateLongTodoChain(value: unknown): JsonObject { const {frontier_revision, frontier_revision_complete, frontier_owned_identity, ...counts} = observation; const receipt = triggerCheckpoint({kind: TRIGGER, frontier_revision, frontier_revision_complete, frontier_owned_identity}); - return {observation: {...observation, trigger: {...counts, ...receipt}}, + return {observation: {...observation, trigger: {...counts, ...receipt, + ...(revision.complete ? {obligation_identity_revision: + revision.frontier_owned_identity ?? revision.frontier_revision} : {})}}, decision: classifyAck(observation, request.ack)}; } diff --git a/loopx/control_plane/work_items/autonomous_replan_obligation.py b/loopx/control_plane/work_items/autonomous_replan_obligation.py index 38a30d6754..86a4119df4 100644 --- a/loopx/control_plane/work_items/autonomous_replan_obligation.py +++ b/loopx/control_plane/work_items/autonomous_replan_obligation.py @@ -203,7 +203,10 @@ def ensure_replan_novelty_policy( normalized.pop("rearmed_after_obligation_id", None) trigger_identity = [ { - key: trigger.get(key) + key: ( + trigger.get("obligation_identity_revision", trigger.get(key)) + if key == "frontier_revision" else trigger.get(key) + ) for key in ( "kind", "frontier_identity", diff --git a/loopx/control_plane/work_items/replan_semantics.ts b/loopx/control_plane/work_items/replan_semantics.ts index eecced5b93..6155461adf 100644 --- a/loopx/control_plane/work_items/replan_semantics.ts +++ b/loopx/control_plane/work_items/replan_semantics.ts @@ -36,8 +36,13 @@ export function requiredSemanticOutcomes(obligation: JsonObject): SemanticOutcom return [...new Set(declared)] as SemanticOutcome[]; } const triggers = Array.isArray(obligation.triggers) ? obligation.triggers : []; - return [...(triggers.some(trigger => VISION_TRIGGERS.has(String(object(trigger).kind ?? "").trim())) - ? VISION_OUTCOMES : PROGRESS_OUTCOMES)]; + const kinds = triggers.map(trigger => String(object(trigger).kind ?? "").trim()); + if (kinds.some(kind => VISION_TRIGGERS.has(kind))) return [...VISION_OUTCOMES]; + // Reviewing a long chain may retain existing runnable work. Its projected + // vision decision must close the checkpoint without manufacturing another + // successor or progress identifier. Keep previously legal progress exits. + return kinds.includes("long_todo_chain") + ? ["fresh_vision_path_outcome", ...PROGRESS_OUTCOMES] : [...PROGRESS_OUTCOMES]; } function writebackProjection(required: SemanticOutcome[]): JsonObject { @@ -51,7 +56,7 @@ function writebackProjection(required: SemanticOutcome[]): JsonObject { vision_authoring: visionAuthoringContract(), required_fields: ["vision_patch.acceptance_summary", "path_delta.outcome", "path_delta.evidence_refs"], path_outcomes: [...FRESH_PATH_DISPOSITIONS], - rule: "Author the JSON file from observed evidence, then execute the bound refresh and spend. A new progress identifier or unchanged reason alone cannot resolve this vision obligation. Other required_any_of exits remain subject to their typed contracts.", + rule: "Author the JSON file from observed evidence, then execute the bound refresh and spend. This path requires an acceptance summary and evidence-linked path outcome; an unchanged reason alone is insufficient. Other required_any_of exits remain subject to their typed contracts.", }, }; } diff --git a/tests/control_plane/test_canonical_frontier_revision.py b/tests/control_plane/test_canonical_frontier_revision.py index 5a60ef2081..e00191768a 100644 --- a/tests/control_plane/test_canonical_frontier_revision.py +++ b/tests/control_plane/test_canonical_frontier_revision.py @@ -59,7 +59,8 @@ def test_complex_canonical_frontier_ack_tracks_only_selectable_material_changes( "role": "agent", "status": "open", "done": False, "task_class": "advancement_task", "text": "Synthetic independent work", "archive_state": "active", "source_section": "Agent Todo", "index": len(projection["todos"]) + 1, "updated_at": "2026-09-01T00:00:00.000002Z", - **({"claimed_by": "agent-a"} if index == 0 else {}), + # Reach the lane-owned threshold; retain a shared tail for ACK parity. + **({"claimed_by": "agent-a"} if index < 15 else {}), **({"excluded_agents": ["agent-a"]} if index == 29 else {}), }) from loopx.control_plane.coordination.local_authority_shadow_projection import canonical_bytes diff --git a/tests/control_plane/test_goal_frontier_replan_rules.py b/tests/control_plane/test_goal_frontier_replan_rules.py index 7bbd6f829a..18b04d3d3f 100644 --- a/tests/control_plane/test_goal_frontier_replan_rules.py +++ b/tests/control_plane/test_goal_frontier_replan_rules.py @@ -314,6 +314,8 @@ def derive(rows, ack=None): changed[0]["priority"] = "P0" else: changed[0]["updated_at"] = "2026-08-22T10:00:00+08:00" + # The open Turn must keep its binding even before an ACK is recorded. + assert (derive(changed)["obligation_id"] != original["obligation_id"]) is rearms assert (derive(changed, ack) is not None) is rearms diff --git a/tests/control_plane/test_long_chain_projected_closeout.py b/tests/control_plane/test_long_chain_projected_closeout.py new file mode 100644 index 0000000000..c97ebce7c0 --- /dev/null +++ b/tests/control_plane/test_long_chain_projected_closeout.py @@ -0,0 +1,114 @@ +"""Follow the long-chain packet through one real, durable CLI Turn (#4667).""" +from __future__ import annotations + +import json +from pathlib import Path + +from tests.control_plane.test_quota_settlement_cli import ( + AGENT_ID, GOAL_ID, SELECTED_REPLAN_TODO_ID, TURN_ID, + _configure_selected_todo_replan_fixture, _projected_cli_args, + _run_cli, _spend_run_count, _write_fixture, +) + + +def test_projected_vision_replan_settles_without_a_meta_successor(tmp_path: Path) -> None: + project, runtime, registry = _write_fixture(tmp_path) + _configure_selected_todo_replan_fixture(project, registry) + state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + # Complete material source, independently authored before admission. + source = state.read_text().replace(" -->", " updated_at=2026-08-01T00%3A00%3A00Z -->") + # Fourteen commitments plus a shared candidate is not this lane's long chain. + last_id = "todo_chain_000000000014" + source = source.replace( + f"todo_id={last_id} status=open task_class=advancement_task action_kind=validate claimed_by={AGENT_ID}", + f"todo_id={last_id} status=open task_class=advancement_task action_kind=validate", + ) + state.write_text(source) + + def call(*args): + rc, result = _run_cli(registry, runtime, *args, cwd=project) + assert rc == 0, result.get("error") or result + return result + + def guard(turn): + return call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--turn-instance-id", turn, + "--scan-path", str(project)) + + shared = call("quota", "should-run", "--runtime-profile", "generic_cli", + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID) + assert shared["goal_frontier_projection"]["replan_required"] is False + call("todo", "claim", "--goal-id", GOAL_ID, "--todo-id", last_id, + "--agent-id", AGENT_ID, "--claimed-by", AGENT_ID) + before = guard(TURN_ID) + original = before["heartbeat_recommendation"]["replan_obligation"] + assert [trigger["kind"] for trigger in original["triggers"]] == ["long_todo_chain"] + assert original["triggers"][0]["count_kind"] == "claimed_advancement_todos" + assert before["selected_todo"]["todo_id"] == SELECTED_REPLAN_TODO_ID + actions = before["interaction_contract"]["cli_channel"]["next_cli_actions"] + contract = before["interaction_contract"]["cli_channel"]["replan_settlement_contract"] + assert contract["settlement_binding"] == { + "kind": "todo", "id": SELECTED_REPLAN_TODO_ID, "cli_argument": "--todo-id", + } + assert contract["semantic_obligation"] == { + "kind": "autonomous_replan", "id": original["obligation_id"], + "settlement_bound": False, "discharge": "todo_bound_writeback", + } + refresh, spend = actions + assert "--agent-vision-json" in refresh + assert "--replan-obligation-id" not in refresh + # No invented Todo, progress identifiers, or legacy repair ACK is necessary. + assert "--autonomous-replan-recorded" not in refresh + # Shared-pool churn after admission must not invalidate this Turn's duty. + call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", + "--text", "Validate a separate shared candidate", + "--task-class", "advancement_task", "--action-kind", "validate", + "--target-key", "shared-candidate") + decision = tmp_path / "decision.json" + decision.write_text(json.dumps({ + "schema_version": "goal_vision_replan_contract_v0", + "state": "vision_patch_proposed", + "vision_patch": { + "vision_summary": "Validate the existing bounded slices in dependency order.", + "acceptance_summary": "Each slice has independent validation before dependent work proceeds.", + "advancement_policy": "as_needed", + }, + "path_delta": { + "schema_version": "goal_path_delta_v0", "outcome": "replan", + "prior_assumption": "The long chain needed a bounded review.", + "observed_reality": "The reviewed chain has a runnable validation slice.", + "retained": ["Existing acceptance boundaries"], + "changed": ["Proceed with the first validation slice"], + "evidence_refs": ["evidence:synthetic-chain-review"], + }, + })) + refresh = refresh.replace("", str(decision)) + refreshed = call(*_projected_cli_args(refresh, turn_instance_id=TURN_ID)) + assert refreshed["vision_checkpoint"]["satisfied"] is True + ack = refreshed["autonomous_replan_ack"] + assert ack["recorded"] and ack["semantic_delta"]["accepted"] + assert ack["semantic_delta"]["satisfying_outcomes"] == ["fresh_vision_path_outcome"] + assert ack["semantic_delta"]["trigger_checkpoints"][0]["frontier_revision"] + identity = refreshed["settlement_identity"] + assert identity["todo_id"] == SELECTED_REPLAN_TODO_ID + assert identity["turn_instance_id"] == TURN_ID + persisted = json.loads(Path(refreshed["json_path"]).read_text()) + assert persisted["autonomous_replan_ack"] == ack + assert _spend_run_count(runtime) == 0 + settled = call(*_projected_cli_args(spend, turn_instance_id=TURN_ID)) + assert settled["settlement_result"]["ok"] is True + assert _spend_run_count(runtime) == 1 + assert state.read_text().count("