From 4cf2660c0d4b8e7c567b16948d7addff75bce5e7 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 10:49:10 +0800 Subject: [PATCH 1/3] fix(replan): deliver ordinary declared-work evidence to followthrough Expose bounded ordinary writeback references separately from typed progress and link them to the declared Todo work. Preserve full-source citation checks and unknown execution or utility semantics. Cover scoped corrections, display bounds, replay rejection, and the real File/SQLite result-readback-to-next-comparison journey. Signed-off-by: song --- .../work_items/replan_context.ts | 52 +++++++++++- .../control_plane/work_items/stride_policy.ts | 10 ++- .../test_effective_turn_replan.py | 23 ++++- tests/control_plane_ts/replan_context.test.ts | 83 +++++++++++++++++++ 4 files changed, 157 insertions(+), 11 deletions(-) diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index 61b0177478..2e8400afc3 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -238,16 +238,28 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI if (!decision) return null; const source = objectOrNull(previous._source_facts)!; const subsequent = new Map(); + const ordinary = new Map(); + const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; + const todoIds = plannedTodoIds(source); + const targets = new Set(todoIds); for (const row of scoped) { const facts = objectOrNull(row._source_facts)!; - if (Number(facts.history_position) <= Number(source.history_position) || !row.progress_observation) continue; + if (Number(facts.history_position) <= Number(source.history_position) || facts.quota_spend_commit) continue; + if (!row.progress_observation) { + if (targets.has(String(observationTodo(row)))) { + const ref = "replan-evidence-" + digest(readableEvidenceRow(row)); + if (!ordinary.has(ref)) ordinary.set(ref, {evidence_ref: ref, + generated_at: row.generated_at, todo_id: row.todo_id}); + } + continue; + } const progress = row.progress_observation as JsonObject; if (!subsequent.has(String(progress.fingerprint))) subsequent.set(String(progress.fingerprint), row); } // The complete denominator is retained even when the review details are capped. const observations = [...subsequent.values()]; - const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; - const todoIds = plannedTodoIds(source); + const ordinaryEvidence = [...ordinary.values()]; + const visibleOrdinary = ordinaryEvidence.slice(0, 6); const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); const plannedWork = todoIds.slice(0, 6).map(todoId => { const matched = todos.filter(todo => todo.todo_id === todoId && todo.role === "agent" @@ -258,7 +270,9 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI 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)))}; + .slice(0, 6).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. @@ -279,6 +293,10 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI coverage_ref: (row.progress_observation as JsonObject).fingerprint, })), omitted_observation_count: Math.max(0, observations.length - 6), + // Ordinary writebacks are source evidence, never fabricated typed progress. + // Keep existing progress counts and historical reference identities intact. + ...(ordinaryEvidence.length ? {ordinary_work: {recorded_count: ordinaryEvidence.length, + omitted_count: ordinaryEvidence.length - visibleOrdinary.length, evidence: visibleOrdinary}} : {}), todo_source_available: todoItems != null, planned_work: plannedWork, omitted_planned_work_count: Math.max(0, todoIds.length - plannedWork.length), @@ -287,12 +305,32 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI guidance: "Compare the previous path decision with subsequent work and authoritative outcomes. " + "Recorded progress and changed identifiers do not prove execution, a new mechanism, or goal gain. " + "Use planned_work to compare declared Todo targets with their current state and subsequent observations. " + + (ordinaryEvidence.length ? "Ordinary declared-work evidence is returned separately in ordinary_work; resolve its references before reviewing outcomes. " : "") + "A missing row or observation is unknown, not proof of non-execution; a done Todo does not certify objective gain. " + "Cited evidence is replayed against full scoped history at writeback; this display is not an eligibility boundary. " + "Resolve evidence before deciding what to retain, change, or test next.", }; } +function ordinaryWorkEvidence(follow: JsonObject): JsonObject[] { + if (follow.ordinary_work === undefined) return []; + const ordinary = requireJsonObject(follow.ordinary_work, "ordinary work"); + const evidence = objects(ordinary.evidence, "ordinary work evidence"); + if (evidence.length > 6 || ["recorded_count", "omitted_count"].some(key => + !Number.isSafeInteger(ordinary[key]) || Number(ordinary[key]) < 0) || + Number(ordinary.recorded_count) !== evidence.length + Number(ordinary.omitted_count)) { + throw new EffectRuntimeRequestError("ordinary work requires bounded evidence and complete counts"); + } + const refs = evidence.map(row => { + identifier(row.todo_id, "ordinary work Todo"); + requireNonEmptyString(row.generated_at, "ordinary work generated_at"); + if (Object.hasOwn(row, "coverage_ref")) throw new EffectRuntimeRequestError("ordinary work cannot claim typed progress coverage"); + return identifier(row.evidence_ref, "ordinary work evidence reference"); + }); + if (new Set(refs).size !== refs.length) throw new EffectRuntimeRequestError("ordinary work evidence must be distinct"); + return evidence; +} + export function validateReplanContext(value: unknown): JsonObject { const context = requireJsonObject(value, "replan_context"); if (context.schema_version !== CONTEXT_SCHEMA) { @@ -323,6 +361,7 @@ export function validateReplanContext(value: unknown): JsonObject { throw new EffectRuntimeRequestError("decision followthrough requires the enabled strategy and unknown utility"); } const observations = objects(follow.observations, "decision_followthrough.observations"); + const ordinary = ordinaryWorkEvidence(follow); if (observations.length > 6 || typeof follow.source_history_complete !== "boolean" || ["recorded_progress_count", "omitted_observation_count", "unsequenced_progress_count"].some( key => !Number.isSafeInteger(follow[key]) || Number(follow[key]) < 0) || @@ -351,6 +390,11 @@ export function validateReplanContext(value: unknown): JsonObject { if (!Array.isArray(row.observation_refs) || row.observation_refs.some(ref => !refs.has(ref))) { throw new EffectRuntimeRequestError("planned work requires delivered subsequent observations"); } + if (follow.ordinary_work !== undefined && (!Array.isArray(row.ordinary_evidence_refs) || + row.ordinary_evidence_refs.some(ref => !ordinary.some(evidence => + evidence.evidence_ref === ref && evidence.todo_id === row.todo_id)))) { + throw new EffectRuntimeRequestError("planned ordinary work requires its delivered source evidence"); + } if (row.current !== null) { const current = requireJsonObject(row.current, "planned work current state"); if (!follow.todo_source_available || Object.entries(current).some(([key, value]) => diff --git a/loopx/control_plane/work_items/stride_policy.ts b/loopx/control_plane/work_items/stride_policy.ts index 7d5a424a66..1483ed2c30 100644 --- a/loopx/control_plane/work_items/stride_policy.ts +++ b/loopx/control_plane/work_items/stride_policy.ts @@ -266,13 +266,17 @@ export function strideReviewMismatch(decision: JsonObject, context: JsonObject, if (!previous && review) return "A previous decision review cannot claim an undelivered decision."; if (previous && review) { const rows = (value: unknown): JsonObject[] => Array.isArray(value) ? value as JsonObject[] : []; - const allowed = new Set([...rows(context.evidence), ...rows(follow?.observations)] + const ordinary = rows((follow?.ordinary_work as JsonObject | undefined)?.evidence); + const allowed = new Set([...rows(context.evidence), ...rows(follow?.observations), ...ordinary] .map(row => row.evidence_ref)); for (const row of rows(context.coverage_ledger)) for (const ref of Array.isArray(row.evidence_ids) ? row.evidence_ids : []) allowed.add(ref); const cited = review.evidence_refs as string[]; - const subsequent = new Set(rows(follow?.observations).map(row => row.evidence_ref)); + const subsequent = new Set([...rows(follow?.observations), ...ordinary].map(row => row.evidence_ref)); const planned = rows(follow?.planned_work); - const linked = new Set(planned.flatMap(work => Array.isArray(work.observation_refs) ? work.observation_refs : [])); + const linked = new Set(planned.flatMap(work => [ + ...(Array.isArray(work.observation_refs) ? work.observation_refs : []), + ...(Array.isArray(work.ordinary_evidence_refs) ? work.ordinary_evidence_refs : []), + ])); let hasDeclaredWork = planned.length > 0; if (sourceReview !== undefined) { const source = requireJsonObject(sourceReview, "stride review evidence"); diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 4054843d2e..348c5cdde7 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -463,6 +463,7 @@ def call(*args: str, expected_code: int = 0) -> dict: "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--classification", "validated_progress", "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--recommended-action", "The candidate regressed (error 5 versus reference 2); retain the reference and compare another source.", *(["--progress-result-class", "unchanged", "--progress-evidence-id", f"fixture:negative-result-{index}"] if index == 0 else []), "--no-global-sync", "--suppress-external-sinks") @@ -473,8 +474,15 @@ def call(*args: str, expected_code: int = 0) -> dict: "--agent-id", AGENT_ID, "--turn-instance-id", "stride-next-review", "--scan-path", str(project)) next_obligation = next_guard["autonomous_replan_obligation"] + delivered_result = next_obligation["replan_context"]["decision_followthrough"]["ordinary_work"]["evidence"][0] + observed_result = call("history", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--evidence-ref", delivered_result["evidence_ref"])["evidence"] + assert "regressed (error 5 versus reference 2)" in observed_result["recommended_action"] + # The actor can now discover the actual ordinary result from the review, + # retain the reference, and choose a successor through the existing owner. + next_task = "Compare another source while retaining the reference" if "regressed" in observed_result["recommended_action"] else "Adopt the candidate" next_successor = call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", - "--claimed-by", AGENT_ID, "--text", "Resolve the remaining source uncertainty", + "--claimed-by", AGENT_ID, "--text", next_task, "--task-class", "advancement_task", "--action-kind", "validate", "--target-key", "source-uncertainty", "--replan-obligation-id", next_obligation["obligation_id"]) next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, @@ -498,8 +506,15 @@ def call(*args: str, expected_code: int = 0) -> dict: assert follow["unresolved_planned_work_count"] == 1 last_work = json.loads(Path(work["json_path"]).read_text()) assert "progress_observation" not in last_work - ordinary_ref = next(row["evidence_ref"] for row in next_obligation["replan_context"]["evidence"] - if row["generated_at"] == last_work["generated_at"]) + ordinary_ref = delivered_result["evidence_ref"] + # The optional typed-progress attachment is not required for the Agent to + # discover a declared experiment's ordinary result in its next review. + assert follow["ordinary_work"]["recorded_count"] == 1 + assert follow["ordinary_work"]["evidence"] == [{ + "evidence_ref": ordinary_ref, "generated_at": last_work["generated_at"], + "todo_id": successor["todo_id"], + }] + assert planned[0]["ordinary_evidence_refs"] == [ordinary_ref] from loopx.control_plane.quota.turn_envelope import build_turn_envelope envelope = build_turn_envelope(next_guard) assert envelope["replan_action_packet"]["stride_control"]["decision_followthrough"] == follow @@ -507,7 +522,7 @@ def call(*args: str, expected_code: int = 0) -> dict: "--evidence-ref", previous["evidence_ref"]) assert resolved["evidence"]["stride_decision"] == decision next_decision = {**decision, "reference_context_id": next_control["reference_context_id"], - "reference_obligation_id": next_obligation["obligation_id"]} + "reference_obligation_id": next_obligation["obligation_id"], "action": "switch"} refresh_args = ("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", "--agent-vision-json", str(decision_path), "--classification", "bounded_replan_progress", diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index ae2369ae9a..38bf52307e 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -131,6 +131,89 @@ function subsequent(position: number): JsonObject { const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"}; +type OrdinaryFollowthrough = { + recorded_progress_count: number; execution_and_goal_gain: string; observations: JsonObject[]; + previous_decision: JsonObject; + ordinary_work: {recorded_count: number; omitted_count: number; evidence: JsonObject[]}; + planned_work: {ordinary_evidence_refs: string[]; observation_refs: string[]}[]; +}; + +test("ordinary declared-work evidence reaches followthrough without becoming typed progress", () => { + const decision = reviewed(1); + (decision._source_facts as JsonObject).planned_todo_ids = ["todo-a"]; + const actual: JsonObject = {...subsequent(2), progress_observation: null, + health_check: "The alternate implementation regressed; retain the reference."}; + const ref = (projectReplanContext({...request, rows: [actual]}).evidence as JsonObject[])[0].evidence_ref; + const before = {...subsequent(0), progress_observation: null}; + const other = {...subsequent(3), todo_id: "todo-other", progress_observation: null}; + const accounting = {...actual, generated_at: row(10).generated_at, + _source_facts: {...actual._source_facts as JsonObject, history_position: 10, + quota_spend_commit: {schema_version: "quota_spend_commit_receipt_v0", effect_id: "spent"}}}; + const context = projectReplanContext({...strideRequest, rows: [before, decision, actual, actual, other, accounting]}); + const follow = context.decision_followthrough as OrdinaryFollowthrough; + assert.equal(follow.recorded_progress_count, 0); + assert.deepEqual(follow.observations, []); + assert.equal(follow.execution_and_goal_gain, "unknown"); + assert.equal(follow.previous_decision.evidence_ref, + (projectReplanContext({...strideRequest, rows: [decision]}).decision_followthrough as OrdinaryFollowthrough).previous_decision.evidence_ref); + assert.deepEqual(follow.ordinary_work, {recorded_count: 1, omitted_count: 0, + evidence: [{evidence_ref: ref, generated_at: actual.generated_at, todo_id: "todo-a"}]}); + assert.deepEqual(follow.planned_work[0].ordinary_evidence_refs, [ref]); + assert.deepEqual(follow.planned_work[0].observation_refs, []); + assert.equal(validateReplanContext(context), context); + const review = {previous_review: {reference_decision_ref: follow.previous_decision.evidence_ref, + adoption: "executed", information_gain: "new_evidence", goal_gain: "regressed", + evidence_refs: [ref], lesson: "Reject the alternate implementation, not the experiment evidence."}}; + assert.equal(strideReviewMismatch(review, context), null); + assert.equal((projectReplanContext({...request, operation: "resolve", rows: [actual], evidence_ref: ref}) + .evidence as JsonObject).health_check, actual.health_check); + for (const patch of [ + {ordinary_work: {...follow.ordinary_work, recorded_count: 0}}, + {ordinary_work: {...follow.ordinary_work, omitted_count: -1}}, + {ordinary_work: {...follow.ordinary_work, evidence: [...follow.ordinary_work.evidence, ...follow.ordinary_work.evidence]}}, + {ordinary_work: {...follow.ordinary_work, evidence: [{...follow.ordinary_work.evidence[0], coverage_ref: "invented-progress"}]}}, + {planned_work: [{...follow.planned_work[0], ordinary_evidence_refs: ["evidence:invented"]}]}, + ]) { + assert.throws(() => validateReplanContext({...context, + decision_followthrough: {...follow, ...patch}}), /ordinary work/); + } + assert.equal(projectReplanContext({...request, rows: [decision, actual]}).decision_followthrough, undefined); +}); + +test("ordinary followthrough retains corrections and bounded coverage under source scope", () => { + const decision = reviewed(1); + const current = {goal_id: "goal-a", goal_instance_id: "instance-current"}; + decision._source_facts = {...decision._source_facts as JsonObject, planned_todo_ids: ["todo-a"], goal_ref: current}; + const actual: JsonObject = {...subsequent(2), progress_observation: null, health_check: "The candidate initially appeared useful."}; + actual._source_facts = {...actual._source_facts as JsonObject, goal_ref: current}; + const correction: JsonObject = {...subsequent(3), progress_observation: null, + health_check: "The delayed result belongs to the earlier candidate; retain the incumbent.", + generated_at: row(0).generated_at}; + correction._source_facts = {...correction._source_facts as JsonObject, goal_ref: current}; + const rows = [decision, actual, correction, + {...actual, goal_id: "foreign-goal"}, {...actual, agent_id: "foreign-agent"}, + {...actual, _source_facts: {...actual._source_facts as JsonObject, goal_ref: {...current, goal_instance_id: "retired"}}}, + {...actual, _source_facts: {...actual._source_facts as JsonObject, history_position: undefined}}]; + const input = {...strideRequest, goal_facts: {goal_ref: current}, rows}; + const follow = projectReplanContext(input).decision_followthrough as OrdinaryFollowthrough; + assert.equal(follow.ordinary_work.recorded_count, 2); + assert.equal(follow.ordinary_work.evidence[0].generated_at, correction.generated_at, + "source order, not the corrected record's older clock, decides subsequent evidence"); + assert.equal(follow.execution_and_goal_gain, "unknown"); + const ref = follow.ordinary_work.evidence[0].evidence_ref; + assert.equal((projectReplanContext({...input, operation: "resolve", evidence_ref: ref}).evidence as JsonObject).health_check, + correction.health_check); + const more = Array.from({length: 8}, (_, i) => { + const value: JsonObject = {...subsequent(i + 4), progress_observation: null}; + return {...value, _source_facts: {...value._source_facts as JsonObject, goal_ref: current}}; + }); + const bounded = projectReplanContext({...input, rows: [...rows, ...more]}).decision_followthrough as OrdinaryFollowthrough; + assert.equal(bounded.ordinary_work.evidence.length, 6); + assert.equal(bounded.ordinary_work.recorded_count, 10); + assert.equal(bounded.ordinary_work.omitted_count, 4); + assert.equal(bounded.recorded_progress_count, 0); +}); + test("cited outcome evidence replays full history beyond display caps and optional progress records", () => { const decision = reviewed(1); decision._source_facts.planned_todo_ids = ["todo-a"]; From a0b20f9865aab76bd8700d35974ff24de266864b Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 10:49:17 +0800 Subject: [PATCH 2/3] docs(replan): define ordinary evidence readback and qualification limits Record the optional followthrough fields in the existing protocol and both stride RFC translations. Distinguish the qualified CLI mechanism from pending provider adoption and research utility. Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 12 +++++++++++ ...rarchical-agent-stride-control-v0.zh-CN.md | 8 +++++++ .../goal-vision-replan-contract-v0.md | 21 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 151fae0b91..ca72e80821 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -771,6 +771,18 @@ The same bounded followthrough is included in `stride_control` so compact Host envelopes expose it without requiring another full-context read. Historical decision identities and off-mode output remain unchanged. +The next-review input also exposes causally later ordinary writebacks for the +declared work in `ordinary_work`, with per-Todo `ordinary_evidence_refs`. Its +six-record detail window retains total/omitted counts and stable history reads; +it does not fabricate a typed-progress fingerprint or increase progress counts. +This closes a source-readback gap where legal evidence could qualify a writeback +but was absent from the decision-followthrough input. Real File/SQLite CLI +coverage uses the returned reference to read a worse candidate result, retain +the incumbent, and author the next comparison through the existing Todo/replan +owners. Late corrections, replay, scope and negative outcomes remain separate +from model judgment. This is not complete M3 provider/product qualification or +evidence of research utility; the §11 actual-provider/adoption journey remains. + Outcome claims require at least one subsequent observation; earlier evidence can still be cited for comparison. Writeback replays up to three cited references against the complete scoped history through the existing typed replan owner. diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 84dba93c07..7e10390cbf 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -639,6 +639,14 @@ Todo 来源缺失和省略数量明确保留;缺少观测不等于没有执行 `stride_control`,紧凑 Host envelope 可直接消费,避免为这些信息再读完整上下文。 历史决定的引用身份及 off 模式输出不变。 +下一轮输入还通过 `ordinary_work` 与各 Todo 的 `ordinary_evidence_refs` 返回已 +声明工作在决定之后的普通回写。详情最多六项,保留总数、省略数和稳定 history +读取,不伪造 typed-progress 指纹或增加其计数。这补齐了“合法证据能通过写回 +校验,却没有进入决定接续输入”的读回缺口。真实 File/SQLite CLI 用返回的引用 +读取较差候选结果、保留原方案,并经既有 Todo/replan owner 创建下一项对照。 +迟到纠正、重放、范围和负结果与模型判断仍明确区分;不宣称完整 M3 provider/ +产品验收或研究收益,§11 的真实 provider 与实际采用路径仍需独立资格化。 + 结果声明至少需要一条决定之后的观测,较早证据仍可作为对照。写回经既有类型化 replan owner 从完整、同范围的历史重放最多三条引用;六条反馈展示及主证据展示 上限均不改变证据资格。普通工作回写不必附带可选 typed-progress 才能作为证据, diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index 2a71929543..e369ad55b9 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -290,6 +290,19 @@ execution, candidate adoption, scientific improvement or Goal completion. A missing, rejected or malformed basis adds no followthrough projection. No selection, trigger, quota, acceptance, lease or scheduler rule changes. +When declared Todo work has later ordinary writebacks without the optional +typed-progress attachment, followthrough also returns `ordinary_work`: up to six +immutable `evidence` references with their `generated_at` and `todo_id`, plus +`recorded_count` and `omitted_count`. Each displayed Todo's +`ordinary_evidence_refs` links only its own delivered records. Accounting-only, +earlier, foreign-lane and retired-instance rows are excluded; append order also +retains delayed corrections without treating their clock as causal order. +Resolve the references through the existing scoped history read before +reviewing the outcome. Ordinary evidence has no `coverage_ref`, does not increase +`recorded_progress_count`, and certifies no execution or gain. Its absence retains +the legacy packet shape, not a claim that no work happened. Feature-off output, +historical identities and full-source writeback qualification are unchanged. + 启用 `evidence_conditioned_v1` 且历史中已有被接受、绑定证据的 Vision 路线时,replan context 返回上次决定与之后的 进度引用,按合格来源的顺序关联,而非根据时间戳或文案猜测因果。完整本地索引提供 追加位置;已知最新在前的紧凑载体与 `newest_first_runs` 入口转换位置,保留普通 @@ -299,6 +312,14 @@ selection, trigger, quota, acceptance, lease or scheduler rule changes. 不等于有效结算 Turn、独立试验或执行/收益证明;后者明确保持 unknown。缺失、 拒绝或损坏的依据不产生投影;选择、触发、结算及权限规则不变。 +已声明 Todo 的后续普通回写即使没有可选 typed-progress,也经 `ordinary_work` +返回最多六条不可变引用、时间和 Todo 身份,并保留总数与省略数。各 Todo 的 +`ordinary_evidence_refs` 只指向自己的已投递记录;排除记账、决定前、其他 Agent +及旧 Goal 实例的记录,迟到纠正仍按来源追加顺序保留。通过既有 history 入口读取 +原文后再评估结果;普通证据不伪造 `coverage_ref`,不增加 typed-progress 计数, +也不证明执行或收益。没有普通证据时保留旧包形状,不据此断言没有工作。off 输出、 +历史身份和完整来源的写回资格不变。 + ### Replan planning guidance The shared `replan_action_packet.planning_guidance` carries five existing Agent From a602f8d34ee4b7260a64517864ab55389637e301 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 11:50:31 +0800 Subject: [PATCH 3/3] feat(replan): retain unresolved work evidence through recorded followthrough Reconstruct result, accepted decision and declared later-work relations from existing scoped history. Citations and reviews alone do not consume results; exact replay cannot resurrect them, and replaced proposals retain their source evidence. Keep execution and utility unknown without a parallel ledger or authority. Carry the real File/SQLite CLI journey through candidate/probe child processes, negative Todo completion and a further evidence-linked review. Document the bounded relation and remaining native model/research qualification. Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 10 +++ ...rarchical-agent-stride-control-v0.zh-CN.md | 6 ++ .../goal-vision-replan-contract-v0.md | 18 ++++ .../work_items/replan_context.ts | 83 +++++++++++++++++++ .../test_effective_turn_replan.py | 52 ++++++++++-- tests/control_plane_ts/replan_context.test.ts | 30 +++++++ 6 files changed, 193 insertions(+), 6 deletions(-) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index ca72e80821..a6e13211a3 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -783,6 +783,16 @@ owners. Late corrections, replay, scope and negative outcomes remain separate from model judgment. This is not complete M3 provider/product qualification or evidence of research utility; the §11 actual-provider/adoption journey remains. +The same source now retains unresolved declared-work evidence across later +reviews. A citation becomes a recorded followthrough relation only after its +decision's declared Todo has a subsequent material writeback; citations or Todo +creation alone do not consume the result. Replay and replaced unexecuted plans +preserve the original evidence without a second ledger. Real File/SQLite CLI +coverage executes isolated candidate/probe processes, reads their artifacts, +authors the next comparison, and reads that relation at another review. The +consumer is scripted: native Host/model adoption, candidate applicability and +scientific utility remain independent, unqualified parts of §11. + Outcome claims require at least one subsequent observation; earlier evidence can still be cited for comparison. Writeback replays up to three cited references against the complete scoped history through the existing typed replan owner. diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 7e10390cbf..ddc5741752 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -647,6 +647,12 @@ Todo 来源缺失和省略数量明确保留;缺少观测不等于没有执行 迟到纠正、重放、范围和负结果与模型判断仍明确区分;不宣称完整 M3 provider/ 产品验收或研究收益,§11 的真实 provider 与实际采用路径仍需独立资格化。 +同一来源现保留跨后续复盘尚待跟进的声明工作证据。只有决定引用结果、声明的 Todo +随后发生材料回写,才记录跟进关联;引用或创建任务本身不消耗结果。重放、替换未执行 +计划均保留原证据,不另建账本。真实 File/SQLite CLI 执行隔离候选/探针进程、读取产物、 +创建下一项对照,并在再下一轮复盘读回关联。消费方仍是脚本,原生 Host/模型采用、 +候选适用性及科学收益继续是 §11 中独立、未资格化的部分。 + 结果声明至少需要一条决定之后的观测,较早证据仍可作为对照。写回经既有类型化 replan owner 从完整、同范围的历史重放最多三条引用;六条反馈展示及主证据展示 上限均不改变证据资格。普通工作回写不必附带可选 typed-progress 才能作为证据, diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index e369ad55b9..fb32a35fd4 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -303,6 +303,17 @@ reviewing the outcome. Ordinary evidence has no `coverage_ref`, does not increas the legacy packet shape, not a claim that no work happened. Feature-off output, historical identities and full-source writeback qualification are unchanged. +`decision_followthrough.work_result_use` also reconstructs unresolved declared-work +evidence across reviews from the existing append order. Another review or a bare +citation cannot consume it: a recorded decision must cite its immutable history +reference, declare subsequent Todo work, and be followed by a material writeback +on that Todo. `recorded_followthrough` exposes that source relation, not certified +execution, candidate applicability or utility. Complete/omitted counts accompany +the six-row windows; replay does not resurrect consumed evidence. A replaced +plan invalidates its unexecuted proposal, not the unresolved source. This is a +read model, not a pending-duty ledger, new write gate or forced continuation. +The Agent still checks the provider's exact candidate/input/result revision. + 启用 `evidence_conditioned_v1` 且历史中已有被接受、绑定证据的 Vision 路线时,replan context 返回上次决定与之后的 进度引用,按合格来源的顺序关联,而非根据时间戳或文案猜测因果。完整本地索引提供 追加位置;已知最新在前的紧凑载体与 `newest_first_runs` 入口转换位置,保留普通 @@ -320,6 +331,13 @@ historical identities and full-source writeback qualification are unchanged. 也不证明执行或收益。没有普通证据时保留旧包形状,不据此断言没有工作。off 输出、 历史身份和完整来源的写回资格不变。 +`work_result_use` 从既有追加历史重建跨复盘尚待跟进的声明工作证据。又做一次复盘 +或只引用一次都不会消耗结果:决定须引用其不可变 history 身份、声明后续 Todo, +并出现该 Todo 的实际材料回写,才返回 `recorded_followthrough` 关联。它不认证执行、 +候选适用性或收益;Agent 仍应核对提供方的候选/输入/结果版本。两个六项窗口保留 +完整/省略计数,重放不复活已关联证据,替换未执行计划不删除原结果。它不是新待办 +账本、写入门槛或强制续跑规则。 + ### Replan planning guidance The shared `replan_action_packet.planning_guidance` carries five existing Agent diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index 2e8400afc3..f909d478ad 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -198,6 +198,63 @@ function observationTodo(row: JsonObject): unknown { return work == null ? row.todo_id : row.todo_id == null || row.todo_id === work ? work : null; } +/** 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, + * not certification that a candidate ran or improved the Goal. */ +function workResultUse(scoped: JsonObject[]): JsonObject | null { + const pending = new Map(); + const origin = new Map(); + const proposed = new Map(); + const followed: JsonObject[] = []; + const seen = new Set(); + for (const row of [...scoped].reverse()) { + const source = objectOrNull(row._source_facts)!; + if (source.quota_spend_commit) continue; + if (acceptedDecisionSource(row)) { + const decision = recordedDecision(row); + if (!decision) { origin.clear(); proposed.clear(); continue; } + const id = String(decision.evidence_ref), targets = plannedTodoIds(source); + const path = decision.path_delta as JsonObject; + const review = objectOrNull(objectOrNull(decision.stride_decision)?.previous_review); + const refs = [...(path.evidence_refs as string[]), + ...(Array.isArray(review?.evidence_refs) ? review.evidence_refs as string[] : [])] + .filter(ref => pending.has(ref)); + for (const todo of targets) { + origin.set(todo, id); + // A newer plan for the same work replaces its unexecuted proposal, + // not the source evidence still waiting for attributable followthrough. + proposed.delete(todo); + if (refs.length) proposed.set(todo, {decision: id, results: [...new Set(refs)]}); + } + continue; + } + const todo = String(observationTodo(row)); + if (!origin.has(todo)) continue; + const ref = "replan-evidence-" + digest(readableEvidenceRow(row)); + if (seen.has(ref)) continue; + seen.add(ref); + const adoption = proposed.get(todo); + if (adoption) { + for (const result of adoption.results) if (pending.delete(result)) followed.push({ + result_evidence_ref: result, decision_ref: adoption.decision, + work_todo_id: todo, work_evidence_ref: ref}); + proposed.delete(todo); + } + if (!pending.has(ref)) pending.set(ref, {evidence_ref: ref, todo_id: todo, + source_decision_ref: origin.get(todo), generated_at: row.generated_at}); + } + if (!pending.size && !followed.length) return null; + const unresolved = [...pending.values()].reverse(), visible = unresolved.slice(0, 6); + return {unresolved_count: unresolved.length, omitted_unresolved_count: unresolved.length - visible.length, + unresolved: visible, recorded_followthrough_count: followed.length, + recorded_followthrough: followed.slice(-6).reverse(), + omitted_followthrough_count: Math.max(0, followed.length - 6), + guidance: "Resolve unresolved evidence even when a newer review has occurred. A cited result stays unresolved until later recorded work is linked through that decision's declared Todo. " + + "Recorded followthrough is an attributable history relation, not verified execution, applicability to a current candidate, or Goal gain. " + + "Check the provider's exact candidate/input/result revision before adopting an outcome; retain valid negative and inconclusive evidence. Existing authority, stopping and settlement rules remain unchanged."}; +} + /** Resolve only the cited refs against full scoped source facts at writeback. * Display limits never decide evidence eligibility. Ordinary work rows do not * need an optional typed progress attachment to remain attributable. */ @@ -260,6 +317,7 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI const observations = [...subsequent.values()]; const ordinaryEvidence = [...ordinary.values()]; const visibleOrdinary = ordinaryEvidence.slice(0, 6); + const resultUse = workResultUse(scoped); const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); const plannedWork = todoIds.slice(0, 6).map(todoId => { const matched = todos.filter(todo => todo.todo_id === todoId && todo.role === "agent" @@ -297,6 +355,7 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI // Keep existing progress counts and historical reference identities intact. ...(ordinaryEvidence.length ? {ordinary_work: {recorded_count: ordinaryEvidence.length, omitted_count: ordinaryEvidence.length - visibleOrdinary.length, evidence: visibleOrdinary}} : {}), + ...(resultUse ? {work_result_use: resultUse} : {}), todo_source_available: todoItems != null, planned_work: plannedWork, omitted_planned_work_count: Math.max(0, todoIds.length - plannedWork.length), @@ -331,6 +390,29 @@ function ordinaryWorkEvidence(follow: JsonObject): JsonObject[] { return evidence; } +function validateWorkResultUse(follow: JsonObject): void { + if (follow.work_result_use === undefined) return; + const use = requireJsonObject(follow.work_result_use, "work result use"); + const pending = objects(use.unresolved, "unresolved work evidence"); + const links = objects(use.recorded_followthrough, "recorded work followthrough"); + if (["unresolved_count", "omitted_unresolved_count", "recorded_followthrough_count", "omitted_followthrough_count"] + .some(key => !Number.isSafeInteger(use[key]) || Number(use[key]) < 0) + || pending.length > 6 || links.length > 6 + || Number(use.unresolved_count) !== pending.length + Number(use.omitted_unresolved_count) + || Number(use.recorded_followthrough_count) !== links.length + Number(use.omitted_followthrough_count)) { + throw new EffectRuntimeRequestError("work result use requires bounded source relations and complete counts"); + } + for (const row of pending) { + for (const key of ["evidence_ref", "todo_id", "source_decision_ref"]) identifier(row[key], key); + requireNonEmptyString(row.generated_at, "unresolved generated_at"); + } + for (const row of links) for (const key of ["result_evidence_ref", "decision_ref", "work_todo_id", "work_evidence_ref"]) identifier(row[key], key); + if (new Set(pending.map(row => row.evidence_ref)).size !== pending.length + || new Set(links.map(row => row.result_evidence_ref)).size !== links.length) { + throw new EffectRuntimeRequestError("work result use requires distinct source results"); + } +} + export function validateReplanContext(value: unknown): JsonObject { const context = requireJsonObject(value, "replan_context"); if (context.schema_version !== CONTEXT_SCHEMA) { @@ -361,6 +443,7 @@ export function validateReplanContext(value: unknown): JsonObject { throw new EffectRuntimeRequestError("decision followthrough requires the enabled strategy and unknown utility"); } const observations = objects(follow.observations, "decision_followthrough.observations"); + validateWorkResultUse(follow); const ordinary = ordinaryWorkEvidence(follow); if (observations.length > 6 || typeof follow.source_history_complete !== "boolean" || ["recorded_progress_count", "omitted_observation_count", "unsequenced_progress_count"].some( diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 348c5cdde7..57cd117791 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -2,6 +2,8 @@ import json import shlex +import subprocess +import sys from pathlib import Path from datetime import datetime, timedelta, timezone @@ -312,7 +314,7 @@ def test_due_stride_review_survives_successor_transition_until_decision( isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) data = json.loads(registry.read_text()) - data["goals"][0]["quota"]["allowed_slots"] = 8 + data["goals"][0]["quota"]["allowed_slots"] = 12 registry.write_text(json.dumps(data)) rc, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) assert rc == 0, listed @@ -455,10 +457,11 @@ def call(*args: str, expected_code: int = 0) -> dict: "--scan-path", str(project)) # Synthetic candidate work under the real selected Todo. A worse # result is still evidence; it must not be rewritten into goal gain. - (project / f"candidate-result-{index}.json").write_text(json.dumps({ - "reference_error": sum(value * value for value in (1, 1)), - "candidate_error": sum(value * value for value in (1, 2)), - })) + subprocess.run([sys.executable, "-c", + "import json,sys; from pathlib import Path; " + "Path(sys.argv[1]).write_text(json.dumps({'reference_error':sum(x*x for x in (1,1))," + "'candidate_error':sum(x*x for x in (1,2))}))", + str(project / f"candidate-result-{index}.json")], check=True) work = call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--classification", "validated_progress", "--delivery-batch-scale", "implementation", @@ -470,6 +473,8 @@ def call(*args: str, expected_code: int = 0) -> dict: call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--slots", "1", "--source", "heartbeat", "--execute") + call("todo", "complete", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", successor["todo_id"]) next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "stride-next-review", "--scan-path", str(project)) @@ -480,7 +485,8 @@ def call(*args: str, expected_code: int = 0) -> dict: assert "regressed (error 5 versus reference 2)" in observed_result["recommended_action"] # The actor can now discover the actual ordinary result from the review, # retain the reference, and choose a successor through the existing owner. - next_task = "Compare another source while retaining the reference" if "regressed" in observed_result["recommended_action"] else "Adopt the candidate" + measurement = json.loads((project / "candidate-result-1.json").read_text()) + next_task = "Compare another source while retaining the reference" if measurement["candidate_error"] > measurement["reference_error"] else "Adopt the candidate" next_successor = call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", "--claimed-by", AGENT_ID, "--text", next_task, "--task-class", "advancement_task", "--action-kind", "validate", @@ -502,6 +508,7 @@ def call(*args: str, expected_code: int = 0) -> dict: planned = follow["planned_work"] assert len(planned) == 1 and planned[0]["todo_id"] == successor["todo_id"] assert planned[0]["current"]["claimed_by"] == AGENT_ID + assert planned[0]["current"]["status"] == "done", "A negative experiment can end while the Goal continues" assert len(planned[0]["observation_refs"]) == 1 assert follow["unresolved_planned_work_count"] == 1 last_work = json.loads(Path(work["json_path"]).read_text()) @@ -523,6 +530,7 @@ def call(*args: str, expected_code: int = 0) -> dict: assert resolved["evidence"]["stride_decision"] == decision next_decision = {**decision, "reference_context_id": next_control["reference_context_id"], "reference_obligation_id": next_obligation["obligation_id"], "action": "switch"} + vision_packet = {**vision_packet, "todo_delta": [f"create:{next_successor['todo_id']}"]} refresh_args = ("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", "--agent-vision-json", str(decision_path), "--classification", "bounded_replan_progress", @@ -546,6 +554,38 @@ def call(*args: str, expected_code: int = 0) -> dict: assert next_accepted["autonomous_replan_ack"]["semantic_delta"]["accepted"] is True replay = call(*refresh_args) assert replay.get("appended") is not True + # A review/citation alone leaves this result pending. Actual subsequent + # work under the declared next Todo supplies the recorded association. + call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", + "--slots", "1", "--source", "heartbeat", "--execute") + for index in range(2): + turn = f"adopted-probe-{index}" + guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--scan-path", str(project)) + assert guard["selected_todo"]["todo_id"] == next_successor["todo_id"] + subprocess.run([sys.executable, "-c", + "import json,sys; from pathlib import Path; " + "old=json.loads(Path(sys.argv[1]).read_text()); " + "Path(sys.argv[2]).write_text(json.dumps({'retained_error':old['reference_error'],'alternative_error':1}))", + str(project / "candidate-result-1.json"), str(project / f"adopted-probe-{index}.json")], check=True) + call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--classification", "validated_progress", "--delivery-batch-scale", "implementation", + "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--recommended-action", "The alternate probe has an attributable artifact; validate transfer before replacing the incumbent.", + "--no-global-sync", "--suppress-external-sinks") + call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--slots", "1", "--source", "heartbeat", "--execute") + final = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--turn-instance-id", "review-adopted-probe", "--scan-path", str(project)) + use = final["autonomous_replan_obligation"]["replan_context"]["decision_followthrough"]["work_result_use"] + assert any(row["result_evidence_ref"] == ordinary_ref and row["work_todo_id"] == next_successor["todo_id"] + for row in use["recorded_followthrough"]) + assert final["autonomous_replan_obligation"]["replan_context"]["decision_followthrough"]["execution_and_goal_gain"] == "unknown" + assert json.loads((project / "adopted-probe-1.json").read_text())["retained_error"] == 2 @pytest.mark.parametrize("invalid", [0, 7, True, 2.5, "2"]) diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index 38bf52307e..2dd917d11a 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -131,6 +131,36 @@ function subsequent(position: number): JsonObject { const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"}; +test("unconsumed result survives later reviews and citations alone; recorded work closes only its source relation", () => { + const first = reviewed(1); + (first._source_facts as JsonObject).planned_todo_ids = ["todo-a"]; + const result = {...subsequent(2), progress_observation: null}; + const ref = (projectReplanContext({...request, rows: [result]}).evidence as JsonObject[])[0].evidence_ref; + const next = reviewed(3); + (next._source_facts as JsonObject).planned_todo_ids = ["todo-b"]; + const vision = (next._source_facts as JsonObject).agent_vision as JsonObject; + (vision.path_delta as JsonObject).evidence_refs = [ref]; + const readUse = (rows: JsonObject[]) => (projectReplanContext({...strideRequest, rows}) + .decision_followthrough as JsonObject).work_result_use as JsonObject; + const proposed = readUse([first, result, next]); + assert.equal(proposed.unresolved_count, 1); + assert.equal((proposed.unresolved as JsonObject[])[0].evidence_ref, ref); + assert.deepEqual(proposed.recorded_followthrough, []); + const actual = {...subsequent(4), todo_id: "todo-b", progress_observation: null}; + const adopted = readUse([first, result, next, actual]); + assert.equal(adopted.unresolved_count, 1, "the next result remains available for the next decision"); + assert.notEqual((adopted.unresolved as JsonObject[])[0].evidence_ref, ref); + const links = adopted.recorded_followthrough as JsonObject[]; + assert.equal(links.length, 1); assert.equal(links[0].result_evidence_ref, ref); + assert.equal(links[0].work_todo_id, "todo-b"); + assert.deepEqual(readUse([first, result, next, actual, actual]), adopted, "replay cannot consume or duplicate twice"); + const replayedResult = {...result, _source_facts: {...result._source_facts as JsonObject, history_position: 5}}; + assert.deepEqual(readUse([first, result, next, actual, replayedResult]), adopted, "a lost-response replay cannot resurrect consumed evidence"); + const omitted = reviewed(3); + (omitted._source_facts as JsonObject).planned_todo_ids = ["todo-b"]; + assert.equal(readUse([first, result, omitted]).unresolved_count, 1, "another review cannot silently drop an uncited result"); +}); + type OrdinaryFollowthrough = { recorded_progress_count: number; execution_and_goal_gain: string; observations: JsonObject[]; previous_decision: JsonObject;