diff --git a/ouroboros/task_results.py b/ouroboros/task_results.py index 99ad4fca8..27e12c798 100644 --- a/ouroboros/task_results.py +++ b/ouroboros/task_results.py @@ -1502,9 +1502,11 @@ def record_plan_review_wave( for idx, w in enumerate(waves) ] overflow = max(0, len(waves) - _PLAN_REVIEW_MAX_WAVES) - if overflow: - state["waves_omitted"] = int(state.get("waves_omitted") or 0) + overflow - waves = waves[overflow:] + if overflow: # the newest PAID wave stays reachable: the next dispatch judges against it + keep = next((i for i in range(len(waves) - 1, -1, -1) if waves[i].get("paid")), None) + dropped = set([i for i in range(len(waves)) if i != keep][:overflow]) + state["waves_omitted"] = int(state.get("waves_omitted") or 0) + len(dropped) + waves = [w for i, w in enumerate(waves) if i not in dropped] # I-02: size-fitting (older-wave compaction, then the last-resort text cut) runs for # EVERY writer in `_update_plan_review_state` → `_fit_plan_review_state`. state["waves"] = waves diff --git a/tests/test_plan_standing_lineage.py b/tests/test_plan_standing_lineage.py index d60833447..b94620c79 100644 --- a/tests/test_plan_standing_lineage.py +++ b/tests/test_plan_standing_lineage.py @@ -164,6 +164,32 @@ def test_a_predecessor_without_an_operative_spec_is_unresolved_history_not_a_cha assert _walk([], None) == {} # no predecessor at all is a first wave +def test_the_last_paid_wave_survives_hot_index_eviction(harness, monkeypatch): + """The hot index keeps at most 64 waves; eviction drops the oldest entries but never the + newest PAID wave, so after 64 unpaid revisions the next revision still judges against A + and a refused objector carries A's finding (the 63/64 boundary).""" + from ouroboros.task_results import _PLAN_REVIEW_MAX_WAVES + + monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "20") + _patch_health(monkeypatch, lambda slots: {}) + _effort_aware_builder(harness, monkeypatch) + harness.install({"s1": _objection("n1", "Friday is impossible"), "s2": CLEAN, "s3": CLEAN}) + ctx = harness.make_ctx() + assert _control(_call(ctx)) == {"outcome": "REVIEW_REQUIRED", "closed": False} + a_fp = _state(harness)["waves"][-1]["request_fingerprint"] + for n in range(_PLAN_REVIEW_MAX_WAVES): + _install_barrier_substrate(monkeypatch, [], refused={"s1", "s2", "s3"}) + _call(ctx, plan=f"Revision {n}: outline, then draft.") + _collect(ctx, _state(harness)["waves"][-1]["request_fingerprint"]) + state = _state(harness) + assert state["waves_omitted"] >= 1 and len(state["waves"]) == _PLAN_REVIEW_MAX_WAVES + assert any(w["request_fingerprint"] == a_fp and w.get("paid") for w in state["waves"]) + _install_barrier_substrate(monkeypatch, [], refused={"s1"}) + _call(ctx, plan="The final revision: draft, then outline.") + assert _control(_collect(ctx, _state(harness)["waves"][-1]["request_fingerprint"])) == {"outcome": "REVIEW_REQUIRED", "closed": False} + assert _carried(harness) == ["s1:n1"] + + def test_a_compacted_last_paid_wave_still_owes_its_objection(harness, monkeypatch): """The hot index keeps eight full waves and compacts older ones. After A (paid, s1 objecting) and eight unpaid revisions (every seat refused at $0), A is compact; the