diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md index 60a03fa168..a0aa4ec56e 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.md @@ -913,6 +913,15 @@ from local sharing to one call. These are projection-parameter measurements, not framed traffic, latency, model billing or the integrated C1 comparison. Retain this negative result rather than declaring a net efficiency win. +The compatibility owner also uses one local JSON serialization identity for +acceptance, Vision-audit and replan-authoring sharing. Python's ordinary value +equality must not turn distinct booleans/numbers, integer/float representations +or signed zero into an alias to another observation. Exact copies still share +one in-response owner, including after key reordering; different facts remain +complete. This repairs source fidelity without another projection RPC. It does +not qualify the unified writer, final consumers or original-quota outcome/cost +benefit. + Repair relocated acceptance aliases at the current compatibility owner without that extra transport. Existing aliases must follow only inline owners actually moved by the projection, including aliases visited before their owner. Preserve diff --git a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md index f1d605a147..2f3fbb1b95 100644 --- a/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md +++ b/docs/architecture/rfcs/controller-executor-unified-contract-v0.zh-CN.md @@ -691,6 +691,12 @@ compactor:精确共享、省略和延后读取的信息与失败后果各不 一次请求。这些是投影参数测量,不是带协议包装的流量、延迟、模型计费或完整 C1 对照。 保留此负结果,不能宣称净效率收益。 +兼容 owner 也为验收、Vision audit 和 replan authoring 共享复用一份本地 JSON 序列化 +身份。不能用 Python 普通值相等,把不同布尔/数值、整数/浮点表示或带符号零替换成 +另一条观测的别名。完全相同的副本仍共享同一响应内的 owner,包括键顺序不同的情况; +不同事实保留全文。这修复来源保真,不增加投影 RPC;不代表统一 writer、最终消费者 +或相对原 quota 的成果/完整成本收益已经合格。 + 先在当前兼容 owner 修复被迁移的验收引用,不引入上述额外传输。已有别名只跟随本次 投影实际迁移的内联 owner,包括先访问别名、后迁移 owner 的情况。保留完整值、原始 数值类型、carrier 顺序和现有大小判断;不推断任意引用链,不重写已存 capture。 diff --git a/docs/reference/required-work-context.md b/docs/reference/required-work-context.md index 056d3c6a04..d11909e968 100644 --- a/docs/reference/required-work-context.md +++ b/docs/reference/required-work-context.md @@ -39,6 +39,11 @@ references follow their complete inline owner when that projection relocates it, including an owner visited later. Different revisions, types, failures and sources stay distinct. This adds no source read or authority, admission or settlement change; saved captures retain their original contents. +Vision-audit and replan-authoring mirrors reuse that local serialization +identity. Boolean/number values, integer/integral-float representations and +signed zero remain distinct when their original JSON encodings differ. Key +order alone does not prevent sharing. This adds no model read or runtime round +trip; distinct source facts retain their complete instructions. Registered Goal state uses the existing writer's strict generated-region ownership. When every recognized Todo region is explicitly marked, the source adapter delivers **all other text verbatim** in `work_context.sources`: Goal @@ -590,6 +595,9 @@ all-Goal adoption, actual solver costs or outcome benefit. 普通 CLI 在同一响应内共享完全相同的验收副本。已有引用跟随本次投影迁移的完整 内联 owner,包括后访问的 owner;不同修订、类型、失败和来源分别保留。不增加来源 读取,不改变权限、准入或结算;已存 capture 保留原始内容。 +Vision audit 和 replan authoring 副本复用该本地序列化身份。原始 JSON 编码不同的 +布尔/数值、整数/整值浮点表示及带符号零分别保留;仅键顺序不同仍可共享。不增加 +模型补读或 runtime 往返;不同来源事实保留完整指引。 Goal Markdown 复用原 writer 对生成区域的严格归属规则:所有已识别 Todo 区域均有 显式标记时,`work_context.sources` 原样携带区域外的全部文字,保留目标、frontmatter、 任意章节、注释、历史和末尾停止条件。`excluded_todo_regions` 保留每段的角色、行区间 diff --git a/loopx/control_plane/quota/cli_projection.py b/loopx/control_plane/quota/cli_projection.py index 72c8a6f501..f56e26ca18 100644 --- a/loopx/control_plane/quota/cli_projection.py +++ b/loopx/control_plane/quota/cli_projection.py @@ -94,6 +94,16 @@ ) +def _json_content_key(value: Any) -> str: + """Compare original JSON values for sharing, including types and signed zero. + + This is local serialization IO, not another quota or context-selection rule. + Python equality merges booleans with integers and integral floats; those + distinct source facts must not borrow each other's in-response owner. + """ + return json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False) + + def _compact_monitor_poll_interaction_contract( contract: Any, ) -> dict[str, Any] | None: @@ -254,10 +264,11 @@ def _reference_inline_replan_authoring( steps = plan.get("ordered_steps") if isinstance(plan, dict) else None if not isinstance(steps, list): return packet + authoring_key = _json_content_key(writeback["vision_authoring"]) index = next( (index for index, step in enumerate(steps) if isinstance(step, dict) and step.get("kind") == "durable_writeback" - and step.get("vision_authoring") == writeback["vision_authoring"]), + and _json_content_key(step.get("vision_authoring")) == authoring_key), None, ) if index is None: @@ -304,10 +315,11 @@ def _replace_nested_vision_continuation_audits( source_audit: dict[str, Any], audit_ref: dict[str, Any], ) -> None: + source_key = _json_content_key(source_audit) frontier = payload.get("goal_frontier_projection") if ( isinstance(frontier, dict) - and frontier.get("vision_continuation_audit") == source_audit + and _json_content_key(frontier.get("vision_continuation_audit")) == source_key ): compact_frontier = dict(frontier) compact_frontier["vision_continuation_audit"] = dict(audit_ref) @@ -321,7 +333,7 @@ def _replace_nested_vision_continuation_audits( channel = compact_interaction.get(channel_name) if ( not isinstance(channel, dict) - or channel.get("vision_continuation_audit") != source_audit + or _json_content_key(channel.get("vision_continuation_audit")) != source_key ): continue compact_channel = dict(channel) @@ -536,9 +548,8 @@ def writable_parent(path: tuple[str | int, ...]) -> dict[str, Any]: value = parent.get(field) if isinstance(parent, dict) else None if not isinstance(value, dict) or not value or field + "_ref" in parent: continue - # Python equality conflates True/1 and False/0. Compare JSON types and - # the complete content, including revision, scope, failures and sources. - encoded = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False) + # Keep complete revision, scope, failures and sources in the identity. + encoded = _json_content_key(value) reference = carriers.get(encoded) own_ref = "$" + "".join( f"[{part}]" if isinstance(part, int) else f".{part}" for part in path) diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 4637f99a68..49a3405df2 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | +| `same_packet_json_type_alias` | Quota sharing replaces a distinct Vision or authoring observation with a reference because Boolean/number or signed-zero values compare equal in the host language. | Original captured values, projected body/ref, exact scalar types and zero signs, equal-content/key-order controls, actual CLI recovery and unchanged receipt/spend counts. | Language-level value equality was used as lossless JSON identity, while another sharing path already retained original serialized types. | Reuse one local serialization identity at the existing compatibility owner; share only complete equal JSON bodies and retain unique values/instructions. Preserve exact references, source input and existing reader/settlement ownership. Cover ordinary/native File/SQLite reads and digest refusal, and count transport rather than adding a late full-payload RPC. Keep installed/model adoption and whole-C1 benefit separate. | | `original_conversation_cli_goal_instance_drift` | `manager-inbox original-conversation` accepts an explicit `--goal-instance-id` but returns success for a stale instance, or follows the currently registered generation instead. | The physical CLI argv, selected registry profile, explicit versus omitted instance, current Goal reference, and real HTTP/source/queue/return readback in disposable File/SQLite state. | The CLI branch dropped the selector before the existing collaboration owner; current-instance inference concealed the missing caller reference. An unsupported source profile is a separate boundary, not proof that forwarding alone enables it. | Validate and forward the explicit Goal reference through the existing owner. Preserve omitted-selector compatibility and keep stale-instance, source/grant and audience checks authoritative. Cover real CLI discovery, request and control, unchanged inferred discovery, stale-instance rejection without queued work, and original recipient/result return; do not promote profiles or infer grants to make a live request pass. | | `conversation_inventory_refusal_reason_loss` | A delegation inventory stays partial because an original request is rejected as `initial_delivery_unproved`, but the App shows only a generic unreadable count. | The current authorized source, committed request/route, trusted originating Turn receipt and typed lifecycle refusal; distinguish incomplete scans and unloaded pages. | The IO adapter collapses a typed receipt conflict into a generic exception and omits the row without retaining a diagnosable identity. | Preserve the existing TS refusal and expose bounded diagnostic identities only after source/route agreement is proved and current audience/Goal checks pass. Keep the count partial, brief/work withheld and controls/return rejected; disclose the cause through existing accessible help. Other failures remain unknown. Cover File/SQLite real HTTP, source/scope changes, bounded diagnostics and fresh read after source recovery. Never rewrite live receipts, infer cancellation from another target, remove proof checks or claim the historical conflict is repaired. | | `codex_usage_session_rebinding` | Cumulative Codex token usage moves to a different session after another `session_meta`, or replaying the same snapshot loses its recorded duration after repeated metadata. | The explicitly bound synthetic rollout's ordered session ids, first clock, usage snapshot identity, and before/after run-index bytes and aggregate counters. | The usage reader overwrites identity and start time while scanning, allowing later metadata to reinterpret earlier counters. | Require identified metadata before usage, reject conflicting session ids, and retain the first clock for repeated same-id headers. Preserve streaming and the existing booking lock. Cover File/SQLite refusal without an append and repaired same-snapshot zero-delta recovery; never discover another session or rewrite historical usage. | diff --git a/tests/control_plane/test_quota_shared_json_identity.py b/tests/control_plane/test_quota_shared_json_identity.py new file mode 100644 index 0000000000..b89f648146 --- /dev/null +++ b/tests/control_plane/test_quota_shared_json_identity.py @@ -0,0 +1,192 @@ +"""Same-packet sharing must retain distinct JSON facts and complete instructions.""" +from __future__ import annotations + +import json +import math +from copy import deepcopy + +import pytest + +from loopx.control_plane.quota.cli_projection import compact_quota_should_run_cli_payload + + +INSTRUCTIONS = "Preserve every original requirement.\nKeep failure evidence and stop before publication. 完整来源。" +FAMILIES = ("frontier", "agent", "cli", "authoring") + + +def _packet(family, left, right, *, reverse=False): + first = {"instructions": INSTRUCTIONS, "extension": {"observation": left}} + second = {"instructions": INSTRUCTIONS, "extension": {"observation": right}} + if reverse: + second = dict(reversed(second.items())) + if family == "authoring": + return {"replan_action_packet": {"writeback_contract": {"vision_authoring": first}}, + "interaction_contract": {"cli_channel": {"settlement_plan": { + "ordered_steps": [{"kind": "durable_writeback", "vision_authoring": second}]}}}} + if family == "frontier": + mirror = {"goal_frontier_projection": {"vision_continuation_audit": second}} + else: + mirror = {"interaction_contract": {family + "_channel": {"vision_continuation_audit": second}}} + return {"vision_continuation_audit": first, **mirror} + + +def _mirror_parent(packet, family): + if family == "authoring": + return packet["replan_action_packet"]["writeback_contract"], "vision_authoring" + if family == "frontier": + return packet["goal_frontier_projection"], "vision_continuation_audit" + return packet["interaction_contract"][family + "_channel"], "vision_continuation_audit" + + +def _assert_observation(body, expected): + assert body["instructions"] == INSTRUCTIONS + actual = body["extension"]["observation"] + _assert_scalar(actual, expected) + + +def _assert_scalar(actual, expected): + assert type(actual) is type(expected) + assert actual == expected + if isinstance(expected, float) and expected == 0: + assert math.copysign(1, actual) == math.copysign(1, expected) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("left,right", [(True, 1), (False, 0), (1, 1.0), (-0.0, 0.0)], + ids=["boolean-one", "boolean-zero", "integer-float", "signed-zero"]) +def test_sharing_keeps_distinct_json_values(family, left, right): + source = _packet(family, left, right) + original = json.dumps(source, ensure_ascii=False) + result = compact_quota_should_run_cli_payload(source) + parent, field = _mirror_parent(result, family) + assert field in parent, "Distinct source facts must remain inline instead of aliasing another value" + assert field + "_ref" not in parent + assert "projection_ref" not in parent[field] + _assert_observation(parent[field], left if family == "authoring" else right) + if family == "authoring": + owner = result["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][0]["vision_authoring"] + _assert_observation(owner, right) + else: + _assert_observation(result["vision_continuation_audit"], left) + assert json.dumps(source, ensure_ascii=False) == original + assert compact_quota_should_run_cli_payload(result) == result + detailed = compact_quota_should_run_cli_payload(source, include_vision_detail=True) + parent, field = _mirror_parent(detailed, family) + _assert_observation(parent[field], left if family == "authoring" else right) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("native", [False, True], ids=["ordinary", "native"]) +def test_real_guard_and_recovery_keep_distinct_vision_observations(tmp_path, monkeypatch, capsys, provider, native): + """Keep different optional source facts through the real CLI and capture. + + The sole producer injection adds a boolean/integer extension after real + admission. No quota, capture, projection, reader or receipt is mocked. + """ + import loopx.cli_commands.quota as quota + from loopx.cli import main + from tests.control_plane.canonical_authority_fixture import isolate_sqlite_runtime + from tests.control_plane import test_quota_settlement_cli as fixture + + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = fixture._write_fixture(tmp_path, provider=provider) + monkeypatch.chdir(project) + monkeypatch.setenv("CODEX_THREAD_ID", "json-sharing-fixture") + monkeypatch.delenv("CODEX_ROLLOUT_TRACE_ROOT", raising=False) + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + + def call(*args): + code = main([*prefix, *args]) + return code, json.loads(capsys.readouterr().out) + + code, vision = call("refresh-state", "--goal-id", fixture.GOAL_ID, + "--agent-id", fixture.AGENT_ID, "--vision-state", "vision_patch_proposed", + "--vision-summary", "Preserve distinct source observations.", + "--vision-acceptance", "Complete original requirements and retain evidence types.", + "--no-global-sync") + assert code == 0 and vision["ok"], vision + state = project / f".codex/goals/{fixture.GOAL_ID}/ACTIVE_GOAL_STATE.md" + before_state = state.read_bytes() + original_emit, observations = quota._emit_quota_result, [] + + def emit_with_distinct_sources(payload, args, **kwargs): + audit = deepcopy(payload["vision_continuation_audit"]) + audit["sharing_fixture"] = {"observation": True} + payload["vision_continuation_audit"] = audit + other = deepcopy(audit) + other["sharing_fixture"]["observation"] = 1 + payload["goal_frontier_projection"]["vision_continuation_audit"] = deepcopy(other) + for channel in ("agent_channel", "cli_channel"): + payload["interaction_contract"][channel]["vision_continuation_audit"] = deepcopy(other) + observations.append(deepcopy(payload)) + return original_emit(payload, args, **kwargs) + + monkeypatch.setattr(quota, "_emit_quota_result", emit_with_distinct_sources) + capture = tmp_path / "capture" + args = ["quota", "should-run", "--goal-id", fixture.GOAL_ID, + "--agent-id", fixture.AGENT_ID, "--todo-id", fixture.TODO_ID, + "--turn-instance-id", "source-sharing-turn", "--codex-app", + "--decision-output-dir", str(capture)] + if native: + context = tmp_path / "native-context" + context.mkdir(mode=0o700) + args += ["--context-delivery", "on", "--codex-context-dir", str(context)] + code, emitted = call(*args) + assert code == 0 and emitted["ok"] and len(observations) == 1, emitted + original_bytes = (capture / "decision.json").read_bytes() + original = json.loads(original_bytes) + before_receipts = fixture._heartbeat_receipt_count(runtime, "source-sharing-turn") + assert before_receipts == 1 and fixture._spend_run_count(runtime) == 0 + if native: + from pathlib import Path + + recovery = emitted["recovery"] + view = json.loads(Path(recovery["path"]).read_bytes()) + pointers = ["/vision_continuation_audit/sharing_fixture", + "/goal_frontier_projection/vision_continuation_audit/sharing_fixture", + "/interaction_contract/agent_channel/vision_continuation_audit/sharing_fixture", + "/interaction_contract/cli_channel/vision_continuation_audit/sharing_fixture"] + selects = [arg for pointer in pointers for arg in ("--select", pointer)] + code, page = call("quota", "context-read", "--capture-file", recovery["path"], + "--capture-sha256", recovery["sha256"], *selects) + assert code == 0 and page["ok"] and page["eof"], page + values = {row["pointer"]: row["value"] for row in json.loads(page["content"])["selections"]} + for pointer in pointers: + _assert_scalar(values[pointer]["observation"], True if pointer == pointers[0] else 1) + code, refused = call("quota", "context-read", "--capture-file", recovery["path"], + "--capture-sha256", "0" * 64, *selects) + assert code == 1 and refused["ok"] is False + else: + view = emitted + _assert_scalar(view["vision_continuation_audit"]["sharing_fixture"]["observation"], True) + for family in ("frontier", "agent", "cli"): + parent, field = _mirror_parent(view, family) + assert "projection_ref" not in parent[field] + _assert_scalar(parent[field]["sharing_fixture"]["observation"], 1) + expected = original["vision_continuation_audit"] | {"sharing_fixture": {"observation": 1}} + assert parent[field] == expected + assert original["heartbeat_receipt"] == observations[0]["heartbeat_receipt"] + assert (capture / "decision.json").read_bytes() == original_bytes + assert fixture._heartbeat_receipt_count(runtime, "source-sharing-turn") == before_receipts + assert fixture._spend_run_count(runtime) == 0 + assert state.read_bytes() == before_state + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("value", [None, True, 1, 1.0, -0.0]) +def test_sharing_still_reuses_equal_json_with_different_key_order(family, value): + source = _packet(family, value, deepcopy(value), reverse=True) + original = json.dumps(source, ensure_ascii=False) + result = compact_quota_should_run_cli_payload(source) + parent, field = _mirror_parent(result, family) + if family == "authoring": + assert field not in parent + assert parent[field + "_ref"] == ( + "$.interaction_contract.cli_channel.settlement_plan.ordered_steps[0].vision_authoring") + owner = result["interaction_contract"]["cli_channel"]["settlement_plan"]["ordered_steps"][0][field] + else: + assert parent[field]["projection_ref"] == "$.vision_continuation_audit" + owner = result["vision_continuation_audit"] + _assert_observation(owner, value) + assert json.dumps(source, ensure_ascii=False) == original + assert compact_quota_should_run_cli_payload(result) == result