From 759c16a9cf430bb7442b47dd3a5c3336ffb93666 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:05:57 +0800 Subject: [PATCH] fix(context): overlay fresh controls on relocated evidence Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/required-work-context.md | 13 +++- loopx/control_plane/quota/context_delivery.ts | 54 +++++++++++----- .../test_context_delivery_cli.py | 35 +++++++++++ .../control_plane_ts/context_delivery.test.ts | 62 +++++++++++++++++++ 4 files changed, 147 insertions(+), 17 deletions(-) diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index f522708571..fe1dca25ac 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -199,8 +199,17 @@ object keys or exact array indices in the immutable base capture. This avoids resending identical authoring and evidence records when task binding or source ordering moves them to another field. The reference copies the complete exact value, never a similar value or one from the current patch. A subtree containing -any field required as current input cannot use a whole-subtree reference. The -codec chooses a reference only when smaller than its literal or recursive edit. +any field required as current input cannot use a bare whole-subtree reference. +It may instead pair `from` with `edit`: copy the exact base value, then apply +the edit to that copy. Every current and unknown control still appears literally +in the edit. Nested `from` paths remain rooted in the original immutable base, +never the edited copy. This avoids repeating a long reference path for each +unchanged requirement when an admitted reader relocates a complete source. +The codec chooses this overlay only when smaller than the ordinary recursive +edit or complete current literal; its instructions appear only when used. +Readers validate the exact current change tree and complete reconstructed digest; +removing a required current override therefore recovers the current full capture. +Older readers recover the same full capture for an unsupported overlay. Current and unknown controls remain inline. Older string-only readers recover the current full capture for an unsupported structured reference. Missing, malformed or incompatible references recover the same full current capture; diff --git a/loopx/control_plane/quota/context_delivery.ts b/loopx/control_plane/quota/context_delivery.ts index 38a0068586..4b4644f5e1 100644 --- a/loopx/control_plane/quota/context_delivery.ts +++ b/loopx/control_plane/quota/context_delivery.ts @@ -166,7 +166,7 @@ function splitContext(context: JsonObject): Split { * Keep one exact codec for quota and Turn; source-only callers use the same codec. */ const SURFACES = ["work_context", "quota_packet", "turn_envelope"] as const; type Surface = typeof SURFACES[number]; -type Delta = {value: unknown} | {from: Path} | {changed?: Record; values?: Record; removed?: string[]} +type Delta = {value: unknown} | {from: Path; edit?: Delta} | {changed?: Record; values?: Record; removed?: string[]} | {items: Record; values?: Record; length?: number}; const pathKey = (path: Path): string => JSON.stringify(path); @@ -335,7 +335,7 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu for (const path of forced) for (let i = 0; i < path.length; i++) ancestors.add(pathKey(path.slice(0, i))); // Binding or source reordering can relocate already-read authoring. Index // exact strings and structured subtrees from the immutable base. A target - // containing any forced-current descendant cannot use a subtree reference. + // with current descendants needs literal overrides, never a bare reference. const strings = new Map(); type Candidate = {reference: {from: Path}; value: unknown}; const subtrees = new Map}>(); @@ -356,16 +356,19 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu } }; index(before, []); - const visit = (a: unknown, b: unknown, path: Path): Delta | null => { + const visit = (a: unknown, b: unknown, path: Path, relocate = true): Delta | null => { const key = pathKey(path); if (exact.has(key)) return {value: clone(b)}; if (!ancestors.has(key) && equal(a, b)) return null; if (typeof b === "string" && strings.has(b)) return clone(strings.get(b)!); let reference: {from: Path} | undefined; - if (!ancestors.has(key) && (Array.isArray(b) || jsonObject(b))) { - // Most current controls cannot reuse a subtree, and unchanged siblings - // already returned above. Hash only size buckets actually needed by a - // remaining candidate; avoid hashing the complete baseline eagerly. + let overlay: Delta | undefined; + if ((!ancestors.has(key) || relocate && a === undefined) && (Array.isArray(b) || jsonObject(b))) { + // Hash only size buckets needed by a relocated candidate. A complete + // exact base may carry the body while an edit repeats all current leaves. + // Current containers already present at this path use ordinary edits; + // repeatedly indexing their forced descendants adds work without moving + // a body. A newly inserted source is the actual relocation boundary. const bucket = subtrees.get(size(b)); if (bucket && !bucket.indexed) { bucket.indexed = new Map(); @@ -376,14 +379,23 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu } const reusable = bucket?.indexed?.get(digest(b)); // Size and hash lookup are indices: exact equality remains the oracle. - if (reusable && equal(reusable.value, b)) reference = reusable.reference; + if (reusable && equal(reusable.value, b)) { + if (!ancestors.has(key)) reference = reusable.reference; + else if (relocate) { + // Compare against this exact body at its new location. Disabling + // relocation for this node avoids recursion; descendants still use + // the ordinary codec. Forced paths retain their CURRENT literals. + const edit = visit(b, b, path, false); + if (edit) overlay = {...reusable.reference, edit}; + } + } } let delta: Delta; if ((jsonObject(a) || a === undefined) && jsonObject(b)) { const old = (a ?? {}) as JsonObject, next = b as JsonObject; const changed: [string, Delta][] = []; for (const [field, value] of Object.entries(next)) { - const child = visit(Object.hasOwn(old, field) ? old[field] : undefined, value, [...path, field]); + const child = visit(Object.hasOwn(old, field) ? old[field] : undefined, value, [...path, field], relocate && !overlay); if (child) changed.push([field, child]); } const removed = Object.keys(old).filter(field => !Object.hasOwn(next, field)); @@ -392,15 +404,16 @@ function changeTree(before: unknown, after: unknown, forced: Path[]): Delta | nu const old = a as unknown[] | undefined; const items: [string, Delta][] = []; b.forEach((value, i) => { - const child = visit(old?.[i], value, [...path, i]); + const child = visit(old?.[i], value, [...path, i], relocate && !overlay); if (child) items.push([String(i), child]); }); delta = groupCurrentValues({items: Object.fromEntries(items), ...(old?.length !== b.length ? {length: b.length} : {})}, size); } else delta = {value: clone(b)}; // Sizing does not publish a value. Copy only a replacement that is chosen, // rather than every unchanged subtree and every discarded ancestor value. - const result = size(delta) < size({value: b}) ? delta : {value: clone(b)}; - return reference && size(reference) < size(result) ? clone(reference) : result; + let result = size(delta) < size({value: b}) ? delta : {value: clone(b)}; + if (reference && size(reference) < size(result)) result = clone(reference); + return overlay && size(overlay) < size(result) ? overlay : result; }; return visit(before, after, []); } @@ -413,7 +426,8 @@ function applyTree(before: unknown, delta: Delta | null, baseline: unknown = bef if (delta === null) return clone(before); if ("value" in delta) return clone(delta.value); if ("from" in delta) { - if (Object.keys(delta).length !== 1 || !Array.isArray(delta.from)) throw new Error("invalid baseline reference"); + if (Object.keys(delta).some(key => key !== "from" && key !== "edit") + || !Array.isArray(delta.from)) throw new Error("invalid baseline reference"); let value = baseline; for (const key of delta.from) { if (!(typeof key === "string" || typeof key === "number" && Number.isSafeInteger(key) && key >= 0) @@ -423,7 +437,9 @@ function applyTree(before: unknown, delta: Delta | null, baseline: unknown = bef } if (typeof value !== "string" && !Array.isArray(value) && !jsonObject(value)) throw new Error("reference has no exact baseline string or subtree"); - return clone(value); + // Every nested reference still resolves against the immutable whole base, + // never the overlay or the partially reconstructed current projection. + return delta.edit === undefined ? clone(value) : applyTree(value, delta.edit, baseline); } if ("items" in delta) { if (before !== undefined && !Array.isArray(before)) throw new Error("array baseline missing"); @@ -445,6 +461,12 @@ function applyTree(before: unknown, delta: Delta | null, baseline: unknown = bef return result; } +function hasOverlay(delta: Delta | null): boolean { + if (!delta || "value" in delta) return false; + if ("from" in delta) return delta.edit !== undefined; + return Object.values("items" in delta ? delta.items : delta.changed ?? {}).some(hasOverlay); +} + function targetBinding(request: JsonObject, context: JsonObject, surface: Surface): JsonObject { const scope = requireJsonObject(request.scope, "context delivery scope"); const binding: JsonObject = {projection_version: VERSION, surface}; @@ -498,7 +520,9 @@ export function prepareContextDelivery(request: JsonObject, legacyQuotaFields: r + "Do not also fetch the full projection just to materialize JSON or replace missing Code Mode variables; " + "tool-process memory is separate from retained model context. If required baseline content is unavailable " + "or continuity is uncertain, recover this capture in full before dependent work. " : "") - + "A value replaces its subtree; from copies the exact string, array or object at that path in the base capture, never in current changes. Values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; + + "A value replaces its subtree; from copies the exact string, array or object at that path in the base capture, never in current changes. " + + (hasOverlay(changes) ? "An optional edit then applies to that copied value, with all nested from paths still rooted in the immutable base. " : "") + + "Values assigns complete fields (exact indices with items); changed/items recurse, removed deletes object keys, length sets array length. Lost context triggers automatic full recovery from this capture, never an owner question."}; if (bytes(delta) >= bytes(full("delta_not_smaller"))) return full("delta_not_smaller"); if (!equal(applyTree(prior, changes), context)) return full("reconstruction_failed"); // Native consumers page the wire before they can use its baseline. Deliver diff --git a/tests/control_plane/test_context_delivery_cli.py b/tests/control_plane/test_context_delivery_cli.py index 6fe69c4204..3407911f7e 100644 --- a/tests/control_plane/test_context_delivery_cli.py +++ b/tests/control_plane/test_context_delivery_cli.py @@ -26,6 +26,41 @@ ) +def test_relocated_current_source_overlay_crosses_the_real_typed_codec(tmp_path, monkeypatch): + isolate_sqlite_runtime(tmp_path, monkeypatch) + scope = dict(goal_id="fixture", agent_id="reader", session_id="native", context_epoch="epoch") + acceptance = {"outcome_requirements": {"revision": 1, "outcomes": [{"id": "outcome", + "requirements": [{"id": f"r{i}", "description": "Preserve exact evidence and the original stop. " * 8, + "status": "unverified", "agent_review": {"conclusion": "uncertain", "basis_current": False, + "note": "An observation is not acceptance. " * 8}} for i in range(24)]}]}} + original = {"planning_horizon": {"core_goal": {"acceptance_contract": acceptance}}} + current = {"interaction_contract": {"agent_channel": {"work_context": {"complete": True, + "sources": [{"source": "goal_acceptance", "content": { + "provider_revision": "current", "goal_acceptance_contract": deepcopy(acceptance)}}]}}}} + args = dict(scope=scope, surface="quota_packet") + first = prepare_context_delivery(original, capture_id="before", **args) + full = prepare_context_delivery(current, capture_id="after", **args) + delta = prepare_context_delivery(current, capture_id="after", baseline=first, + retained={"complete": True, "target": first["target"]}, **args) + assert delta["mode"] == "delta" + overlays = [] + def visit(node): + if not isinstance(node, dict) or "value" in node: + return + if "from" in node and "edit" in node: + overlays.append(node) + for child in node.get("changed", node.get("items", {})).values(): + visit(child) + visit(delta["changes"]) + assert overlays, "Real RPC must carry the registered overlay, not silently downgrade it" + assert restore_context_delivery(delta, baseline=first, current=full)["context"] == current + # The same original capture is the fallback for unsupported/damaged edits. + del overlays[0]["edit"] + restored = restore_context_delivery(delta, baseline=first, current=full) + assert restored["mode"] == "full" and restored["context"] == current + assert first["context"] == original + + @pytest.mark.parametrize("body", ["plain text " * 900, '汉🙂"\\\t\n' * 700]) @pytest.mark.parametrize("limit", [1600, 7000]) def test_reader_bounds_actual_json_stdout_and_preserves_every_byte(tmp_path, monkeypatch, body, limit): diff --git a/tests/control_plane_ts/context_delivery.test.ts b/tests/control_plane_ts/context_delivery.test.ts index f662de04cf..da0ec827ec 100644 --- a/tests/control_plane_ts/context_delivery.test.ts +++ b/tests/control_plane_ts/context_delivery.test.ts @@ -78,6 +78,9 @@ const recover = (delivery: JsonObject, baseline: JsonObject, value: JsonObject): function inline(delivery: JsonObject, path: (string | number)[]): unknown { let node = delivery.changes as JsonObject; for (let i = 0; ; i++) { + // An overlay's inherited body is unavailable to this literal-only reader. + // Inspect only its current edit when checking fresh control delivery. + if (node?.from && node.edit) node = node.edit as JsonObject; if (!node) return undefined; if (Object.hasOwn(node, "value")) { let value = node.value; @@ -264,6 +267,65 @@ test("relocated immutable records reuse complete subtrees without hiding fresh d current: prepareContextDelivery(request)}).context, request.context); }); +test("relocated evidence overlays keep current controls literal without repeating reference paths", () => { + const requirements = Array.from({length: 24}, (_, i) => ({id: `r${i}`, + description: `Requirement ${i}: ${long}`, status: "unverified", planning_satisfied: false, + unknown_control: {may_publish: false}, + agent_review: {conclusion: "uncertain", basis_current: false, + independently_verified: false, note: `Evidence ${i}: ${long}`, evidence_refs: [`artifact:${i}`]}})); + const acceptance = {outcome_requirements: {revision: 1, + outcomes: [{id: "outcome", objective: long, requirements}]}}; + const work = {complete: true, sources: [{source: "goal_acceptance", content: { + provider_revision: "current", goal_acceptance_contract: acceptance}}]}; + // Actual callers relocate acceptance from planning into the admitted source. + const original = {planning_horizon: {core_goal: {objective: long, acceptance_contract: acceptance}}}; + const current = {interaction_contract: {agent_channel: {work_context: work}}}; + const initial = prepareContextDelivery({context: original, scope, capture_id: "a", surface: "quota_packet"}); + const args = {context: current, scope, capture_id: "b", surface: "quota_packet"}; + const delta = prepareContextDelivery({...args, baseline: initial, retained: {complete: true, target: initial.target}}); + assert.equal(delta.mode, "delta"); + const overlays: JsonObject[] = []; + const walk = (value: unknown) => { + if (!value || typeof value !== "object") return; + const item = value as JsonObject; + if (item.from && item.edit) overlays.push(item); + Object.values(item).forEach(walk); + }; + walk(delta.changes); + assert.ok(overlays.length > 0, "relocated evidence should share a base with literal current overrides"); + const path = ["interaction_contract", "agent_channel", "work_context", "sources", 0, "content", + "goal_acceptance_contract", "outcome_requirements"]; + assert.equal(inline(delta, [...path, "revision"]), 1); + requirements.forEach((row, i) => { + const prefix = [...path, "outcomes", 0, "requirements", i]; + assert.equal(inline(delta, [...prefix, "status"]), row.status); + assert.equal(inline(delta, [...prefix, "planning_satisfied"]), row.planning_satisfied); + assert.equal(inline(delta, [...prefix, "unknown_control", "may_publish"]), false); + for (const field of ["conclusion", "basis_current", "independently_verified"]) + assert.equal(inline(delta, [...prefix, "agent_review", field]), (row.agent_review as JsonObject)[field]); + }); + assert.deepEqual(restoreContextDelivery({delivery: delta, baseline: initial, + current: prepareContextDelivery(args)}).context, current); + assert.deepEqual(initial.context, original); + // Losing a current override, redirecting the base, or adding a patch-local + // reference must recover the current original, never revive inherited control. + for (const mutate of [(item: JsonObject) => { delete item.edit; }, + (item: JsonObject) => { item.from = ["constructor", "prototype"]; }, + (item: JsonObject) => { item.edit = {from: ["changes"]}; }]) { + const damaged = structuredClone(delta), found: JsonObject[] = []; + const find = (value: unknown) => { + if (!value || typeof value !== "object") return; + const item = value as JsonObject; + if (item.from && item.edit) found.push(item); + Object.values(item).forEach(find); + }; + find(damaged.changes); mutate(found[0]); + const result = restoreContextDelivery({delivery: damaged, baseline: initial, current: prepareContextDelivery(args)}); + assert.equal(result.mode, "full"); + assert.deepEqual(result.context, current); + } +}); + test("exact-base source and requirement reorders preserve current positions", () => { const initial = context(); const outcomes = ((source(initial, 2).goal_acceptance_contract as JsonObject).outcome_requirements as JsonObject).outcomes as JsonObject[];