From 350ba6a23b9a323da88b860bc9f19879c31a9285 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:46:47 +0800 Subject: [PATCH] Reuse retained outcome provenance in context deltas Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 14 +++++++++---- loopx/control_plane/quota/context_delivery.ts | 19 ++++++++++------- .../test_context_delivery_cli.py | 18 ++++++++++++++++ .../control_plane_ts/context_delivery.test.ts | 21 ++++++++++++++++++- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 0417ac1e0d..8314169b51 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -262,12 +262,16 @@ branch identity, selection, status, resume facts and orchestration gates remain current. Reordering, refutation, removed evidence and changed guidance reconstruct the exact current source; no timestamp, revision or text is normalized away. -Within successfully read cumulative Goal outcomes, source summaries, review notes -and string evidence references also use exact reuse. They are provenance and -explanations, not current acceptance decisions. Requirement status, review +Within successfully read cumulative Goal outcomes, string source references, +source summaries, review notes and string evidence references also use exact +reuse. They are provenance and explanations, not current acceptance decisions. Requirement status, review conclusion, basis currency, independent verification, revisions and unknown fields remain literal current input. Changed, removed or reordered evidence reconstructs the complete current source; failed or incomplete sources retain full delivery. +Containers emptied solely by removing reusable fields do not force those fields +back into the delta. Originally empty unknown records and non-string source +references remain literal current input. Reuse keeps the locator's exact value; +it neither rereads the referenced material nor refreshes its authority. The registered preference owner permits exact reuse of its complete instructions, statements, source quotes and source references. Each read still supplies the @@ -598,10 +602,12 @@ TurnEnvelope。在基线连续性成立的口径内,两入口仍逐轮发送 ` 条件、身份、命令与未知控制仍逐轮发送。Explore owner 登记分支证据审计与证据关联 模板的精确复用;分支身份、选择、状态、恢复事实与编排门仍是当前输入。重排、反证、 移除与指导变更完整重建当前来源,不规范化时间戳、修订或正文。 -已成功完整读取的累计 Goal outcome 中,来源说明、复核笔记及字符串证据引用也按精确 +已成功完整读取的累计 Goal outcome 中,字符串来源引用、来源说明、复核笔记及字符串证据引用也按精确 差分传输。它们是来源与解释;要求状态、复核结论、依据是否当前、独立验证标记、修订 及未知字段仍逐轮完整发送。证据变化、移除或重排后仍须精确还原当前来源;来源失败或 读取不完整时保留完整投递。 +仅因移除可复用字段而变空的容器,不会把这些字段重新带回差分;原本为空的未知对象和 +非字符串来源引用仍逐轮发送。复用保留定位符的精确值,不重读其指向的材料,也不刷新权限。 对象编辑可以在 `values` 中合并携带本轮完整字段,同时保留递归 `changed` 与删除 `removed`;数组的 `items` 与 `values` 使用精确索引,`length` 调整长度。 字段仍在本轮输入中,只减少重复 wrapper;codec 比较普通编辑、合并字段 diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 47c092ce45..338f9f23ef 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -121,8 +121,11 @@ function splitContext(context: JsonObject, binding: JsonObject): Split { // controls still reach every input literally. if (Array.isArray(outcome.sources)) { outcome.sources.forEach((entry, index) => { - if (jsonObject(entry)) add(JSON.stringify([kind, outcome.id, "source_summary", index]), - [...path, "sources", index, "summary"]); + const source = jsonObject(entry); + if (!source) return; + add(JSON.stringify([kind, outcome.id, "source_summary", index]), [...path, "sources", index, "summary"]); + if (typeof source.ref === "string") add(JSON.stringify([kind, outcome.id, "source_ref", index]), + [...path, "sources", index, "ref"]); }); } const requirements = outcome.requirements; @@ -193,17 +196,19 @@ const REUSABLE_PACKET_FIELDS = new Set([ function freshPaths(context: JsonObject, surface: Surface, legacyQuotaFields: readonly string[], binding: JsonObject): {paths: Path[]; valid: boolean} { const paths: Path[] = []; let valid = true; - const leaves = (value: unknown, path: Path) => { - if (Array.isArray(value) && value.length) value.forEach((v, i) => leaves(v, [...path, i])); + const leaves = (value: unknown, original: unknown, path: Path) => { + if (Array.isArray(value) && value.length) value.forEach((v, i) => leaves(v, (original as unknown[])[i], [...path, i])); else if (jsonObject(value) && Object.keys(value as JsonObject).length) - Object.entries(value as JsonObject).forEach(([k, v]) => leaves(v, [...path, k])); - else paths.push(path); + Object.entries(value as JsonObject).forEach(([k, v]) => leaves(v, (original as JsonObject)[k], [...path, k])); + // Removing only registered reusable fields leaves an empty container, not + // a current control. An originally empty unknown record still stays literal. + else if (!(jsonObject(value) && jsonObject(original) && Object.keys(original as JsonObject).length)) paths.push(path); }; const work = (value: unknown, path: Path) => { const source = jsonObject(value); if (!source) { paths.push(path); return; } const split = splitContext(source, binding); valid &&= split.valid; - leaves(split.fresh, path); + leaves(split.fresh, source, path); }; const vision = (value: unknown, path: Path) => { const audit = jsonObject(value); diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 3407911f7e..cf6ce69a3c 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -262,6 +262,22 @@ def test_real_cli_host_rendezvous_sends_delta_and_keeps_same_capture_full(tmp_pa initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=todos, handoff_mode="soft_claim"), state_path=state, provider=provider) + state.write_text(state.read_text().replace("status: active-read-only", "status: active")) + settings = json.loads(registry.read_text()) + settings["goals"][0]["status"] = "active" + registry.write_text(json.dumps(settings)) + code, acceptance = _run_cli(registry, runtime, "goal-acceptance", "inspect", "--goal-id", GOAL_ID) + assert code == 0, acceptance + source_ref = "owner:validated-request-" + "original-scope-" * 4 + document = tmp_path / "outcome.json" + document.write_text(json.dumps({"outcome_id": "delivery", "owner_agent_id": AGENT_ID, + "objective": "Deliver the requested artifact", "todo_ids": [TODO_ID], + "requirements": [{"id": "original", "description": "Retain evidence and stop before publication"}], + "source": {"ref": source_ref, "summary": "The original owner requested the artifact."}})) + code, declared = _run_cli(registry, runtime, "goal-acceptance", "refine", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--document", str(document), "--operation-id", "declare-outcome", + "--expected-provider-revision", acceptance["provider_revision"], "--execute") + assert code == 0, declared before_source = state.read_bytes() root = tmp_path / "captures" root.mkdir() @@ -312,10 +328,12 @@ def release(): cold, first = invoke({"scope": scope}) assert cold["mode"] == "full" + assert source_ref in json.dumps(cold["context"]) assert json.loads((first / "projection.json").read_bytes()) == cold["context"] warm, second = invoke({"scope": scope, "baseline": cold, "retained": {"complete": True, "target": cold["target"]}}) assert warm["mode"] == "delta" + assert source_ref not in json.dumps(warm["changes"]) projection = json.loads((second / "projection.json").read_bytes()) current = prepare_context_delivery(projection, scope=scope, capture_id=warm["target"]["capture_id"], surface=surface) diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index 9c8d2b52db..1f806cf760 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -249,7 +249,9 @@ test("fresh control with exact warm bodies; changed source revisions do not rese test("cumulative acceptance evidence reuses exact history while current judgments stay inline on every surface", () => { const evidence = "artifact:validation/" + "evidence-segment/".repeat(80); const outcome = {id: "result", objective: "Preserve the original goal.", revision: 1, - sources: [{ref: "owner:original", summary: long, unknown_source_control: {must_read: true}}], + sources: [{ref: "owner:original-" + "source-segment-".repeat(6), summary: long, + unknown_source_control: {must_read: true}, unknown_empty_control: {}}, + {ref: "owner:second", summary: "A second source."}], requirements: Array.from({length: 12}, (_, i) => ({id: `r${i}`, description: "Verify the result.", status: "unverified", planning_satisfied: false, agent_review: {id: `r${i}`, conclusion: "uncertain", basis_current: false, @@ -278,10 +280,15 @@ test("cumulative acceptance evidence reuses exact history while current judgment assert.equal(inline(delta, [...row, "status"]), "unverified"); assert.equal(inline(delta, [...row, "planning_satisfied"]), false); assert.deepEqual(inline(delta, [...root, "outcomes", 0, "sources", 0, "unknown_source_control"]), {must_read: true}); + assert.deepEqual(inline(delta, [...root, "outcomes", 0, "sources", 0, "unknown_empty_control"]), {}); + assert.equal(inline(delta, [...root, "outcomes", 0, "sources", 0, "ref"]), undefined); + assert.equal(inline(delta, [...root, "outcomes", 0, "sources", 1, "ref"]), undefined); assert.deepEqual(restoreContextDelivery({delivery: delta, baseline, current: prepareContextDelivery(args)}).context, wrap(current)); assert.deepEqual(baseline.context, original); // Changed, reordered, removed and empty evidence never inherit an old claim. updated.outcomes[0].sources[0].summary = "A new owner correction."; + updated.outcomes[0].sources[0].ref = "owner:corrected"; + updated.outcomes[0].sources.reverse(); updated.outcomes[0].requirements.reverse(); updated.outcomes[0].requirements[0].agent_review.note = ""; updated.outcomes[0].requirements[0].agent_review.evidence_refs = ["artifact:new"]; @@ -290,6 +297,18 @@ test("cumulative acceptance evidence reuses exact history while current judgment const edited = {...args, context: wrap(current)}; const next = prepareContextDelivery({...edited, baseline, retained: {complete: true, target: baseline.target}}); assert.deepEqual(restoreContextDelivery({delivery: next, baseline, current: prepareContextDelivery(edited)}).context, wrap(current)); + for (const sources of [updated.outcomes[0].sources.slice(0, 1), [], + [{ref: {unknown_authority: "Do not reuse this control record."}, summary: long}]]) { + const changed = structuredClone(current) as JsonObject; + const outcomes = ((source(changed, 0).goal_acceptance_contract as JsonObject) + .outcome_requirements as JsonObject).outcomes as JsonObject[]; + outcomes[0].sources = sources; + const args = {context: wrap(changed), scope, capture_id: "changed", surface}; + const delta = prepareContextDelivery({...args, baseline, retained: {complete: true, target: baseline.target}}); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline, current: prepareContextDelivery(args)}).context, wrap(changed)); + if (sources.length && typeof sources[0].ref !== "string") + assert.deepEqual(inline(delta, [...root, "outcomes", 0, "sources", 0, "ref"]), sources[0].ref); + } } // An unavailable source is not historical evidence eligible for omission. const unavailable = structuredClone(work) as JsonObject;