From a32592d11f598150ccc004a6c4013e7ea9ced4e1 Mon Sep 17 00:00:00 2001 From: shangzh0 <97216392+shangzh0@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:59:50 +0800 Subject: [PATCH] fix(replan): preserve evidence across declared work items Signed-off-by: shangzh0 <97216392+shangzh0@users.noreply.github.com> --- .../work_items/replan_context.ts | 32 +++++++++++------ .../test_replan_context_order.py | 30 ++++++++++++++++ tests/control_plane_ts/replan_context.test.ts | 35 +++++++++++++++++++ 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index bd47dfd568..fbac5ab5e6 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -202,6 +202,24 @@ function observationTodo(row: JsonObject): unknown { return work == null ? row.todo_id : row.todo_id == null || row.todo_id === work ? work : null; } +/** Reserve one source-ordered observation per displayed planned Todo before + * filling the remaining slots. Repetition on one work item must not hide all + * followthrough on another; the cap is presentation, never evidence eligibility. */ +function boundedWorkEvidence(rows: JsonObject[], todoIds: string[]): JsonObject[] { + const first = new Set(); + const displayed = new Set(todoIds.slice(0, 6)); + for (const id of displayed) { + const row = rows.find(row => observationTodo(row) === id); + if (row) first.add(row); + } + const relevant = rows.filter(row => displayed.has(String(observationTodo(row)))); + for (const row of [...relevant, ...rows]) { + if (first.size >= 6) break; + first.add(row); + } + return rows.filter(row => first.has(row)); +} + /** Recover the result -> decision -> subsequent work relation from the existing * source history. A citation alone does not consume a result, and no extra * ledger or execution authority is created. These are recorded associations, @@ -326,7 +344,8 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI // The complete denominator is retained even when the review details are capped. const observations = [...subsequent.values()]; const ordinaryEvidence = [...ordinary.values()]; - const visibleOrdinary = ordinaryEvidence.slice(0, 6); + const visibleOrdinary = boundedWorkEvidence(ordinaryEvidence, todoIds); + const visible = boundedWorkEvidence(observations, todoIds); const resultUse = workResultUse(scoped); const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); const plannedWork = todoIds.slice(0, 6).map(todoId => { @@ -337,18 +356,11 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI return {todo_id: todoId, current: current ? Object.fromEntries(["status", "claimed_by", "task_class", "action_kind"] .filter(key => Object.hasOwn(current, key)).map(key => [key, current[key]])) : null, - observation_refs: observations.filter(row => observationTodo(row) === todoId) - .slice(0, 6).map(row => "replan-evidence-" + digest(readableEvidenceRow(row))), + observation_refs: visible.filter(row => observationTodo(row) === todoId) + .map(row => "replan-evidence-" + digest(readableEvidenceRow(row))), ...(ordinaryEvidence.length ? {ordinary_evidence_refs: visibleOrdinary .filter(row => row.todo_id === todoId).map(row => row.evidence_ref)} : {})}; }); - // Keep relevant experiment observations in the small visible window even - // when newer unrelated work would otherwise crowd them out. - const linkedRefs = new Set(plannedWork.flatMap(work => work.observation_refs)); - const visible = [...observations.filter(row => linkedRefs.has("replan-evidence-" + digest(readableEvidenceRow(row)))), - ...observations.filter(row => !linkedRefs.has("replan-evidence-" + digest(readableEvidenceRow(row))))].slice(0, 6); - const visibleRefs = new Set(visible.map(row => "replan-evidence-" + digest(readableEvidenceRow(row)))); - for (const work of plannedWork) work.observation_refs = work.observation_refs.filter(ref => visibleRefs.has(ref)); return { previous_decision: decision, source_history_complete: fromFullIndex, diff --git a/tests/control_plane/test_replan_context_order.py b/tests/control_plane/test_replan_context_order.py index 6bc9e657d6..1c0ced4f04 100644 --- a/tests/control_plane/test_replan_context_order.py +++ b/tests/control_plane/test_replan_context_order.py @@ -57,6 +57,36 @@ def test_named_newest_first_builder_preserves_order_without_a_status_source(): _assert_followthrough(context) +@pytest.mark.parametrize("typed", [False, True]) +def test_declared_results_survive_busy_work_through_the_source_codec(typed): + _, decision, work = _rows() + decision["agent_vision"]["todo_delta"] = ["retain:todo-busy", "retain:todo-comparison"] + + def result(todo, index): + item = {**work, "todo_id": todo, "turn_instance_id": f"turn-work-{index}", + "settlement_identity": {**work["settlement_identity"], "todo_id": todo, + "turn_instance_id": f"turn-work-{index}"}, + "health_check": f"Independent measured result {index}."} + if typed: + item["progress_observation"] = {**work["progress_observation"], "work_item_id": todo, + "surface_id": f"surface-{index}"} + else: + item.pop("progress_observation", None) + return item + + rows = [decision, result("todo-comparison", 0), *[result("todo-busy", i + 1) for i in range(12)]] + for strategy in ("off", "evidence_conditioned_v1"): + context = project_replan_context(goal_id="evidence-goal", agent_id="agent-a", runs=rows, + history_newest_first=False, stride_strategy=strategy) + follow = context["decision_followthrough"] + observed = follow["observations"] if typed else follow["ordinary_work"]["evidence"] + assert len(observed) == 6 + assert {row["todo_id"] for row in observed} == {"todo-busy", "todo-comparison"} + planned = next(work for work in follow["planned_work"] if work["todo_id"] == "todo-comparison") + assert len(planned["observation_refs"] if typed else planned["ordinary_evidence_refs"]) == 1 + assert follow["execution_and_goal_gain"] == "unknown" + + def test_real_append_index_ignores_compact_display_order_and_clock_skew(tmp_path): registry = tmp_path / "registry.json" registry.write_text(json.dumps({"goals": [{"id": "evidence-goal", "repo": str(tmp_path)}]})) diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index b80daf6356..e9372ad355 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -338,6 +338,41 @@ test("legacy descriptive Todo deltas stay readable and unresolved", () => { assert.equal(validateReplanContext(context), context); }); +test("busy planned work cannot crowd another declared experiment out of review", () => { + const decision = reviewed(1); + decision._source_facts.planned_todo_ids = ["todo-busy", "todo-counterexample"]; + for (const typed of [true, false]) { + const observation = (position: number, todo: string): JsonObject => ({...subsequent(position), todo_id: todo, + health_check: todo === "todo-counterexample" ? "The comparison rejected the proposed explanation." : "Repeated validation.", + progress_observation: typed ? {...progress, work_item_id: todo, fingerprint: `result-${position}`} : null}); + const counterexample = observation(2, "todo-counterexample"); + const rows = [decision, counterexample, ...Array.from({length: 12}, (_, i) => observation(i + 3, "todo-busy"))]; + const input = {...strideRequest, rows}; + const context = projectReplanContext(input); + const follow = context.decision_followthrough; + const evidence = typed ? follow.observations : follow.ordinary_work.evidence; + assert.equal(evidence.length, 6); + assert.deepEqual(evidence.map((row: JsonObject) => row.todo_id), + [...Array(5).fill("todo-busy"), "todo-counterexample"], "source order is retained within the balanced window"); + const ref = evidence[5].evidence_ref; + const work = follow.planned_work[1]; + assert.deepEqual(typed ? work.observation_refs : work.ordinary_evidence_refs, [ref]); + assert.equal(typed ? follow.omitted_observation_count : follow.ordinary_work.omitted_count, 7); + assert.equal(typed ? follow.recorded_progress_count : follow.ordinary_work.recorded_count, 13); + assert.equal(validateReplanContext(context), context); + assert.equal(projectReplanContext({...input, operation: "resolve", evidence_ref: ref}).evidence.health_check, + counterexample.health_check); + const previous = follow.previous_decision.evidence_ref; + const review = {previous_review: {reference_decision_ref: previous, adoption: "executed", + information_gain: "new_evidence", goal_gain: "unknown", evidence_refs: [ref], lesson: "Retain the negative comparison."}}; + const replay = projectReplanContext({...input, operation: "review_evidence", previous_decision_ref: previous, evidence_refs: [ref]}); + assert.equal(strideReviewMismatch(review, context, replay), null); + assert.deepEqual(projectReplanContext({...input, stride_strategy: "off"}).decision_followthrough, follow); + assert.deepEqual(projectReplanContext({...input, rows: [...rows].reverse()}).decision_followthrough, follow); + assert.equal(follow.execution_and_goal_gain, "unknown"); + } +}); + test("declared work returns canonical state and only its subsequent observations", () => { const decision = reviewed(2); decision._source_facts.planned_todo_ids = ["todo-experiment", "todo-unavailable"];