diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index a25b34f58f..040251b1de 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -121,6 +121,11 @@ def __init__(self) -> None: self._revision: _RuntimeRevision | None = None self._scope_count = 1 self._closed = False + # The Todo context handler is a pure lens over supplied records. Share + # its exact results only inside this already revision-pinned request; + # authority reads, clocks, mutation and settlement never enter here. + self._todo_context: dict[bytes, bytes] = {} + self._todo_context_bytes = 0 def try_join(self) -> bool: with self._lock: @@ -143,6 +148,20 @@ def leave(self) -> None: if self._scope_count == 0: self._closed = True self._revision = None + self._todo_context.clear() + self._todo_context_bytes = 0 + + def todo_context_result(self, key: bytes) -> bytes | None: + with self._lock: + return None if self._closed else self._todo_context.get(key) + + def remember_todo_context(self, key: bytes, result: bytes) -> None: + with self._lock: + size = len(key) + len(result) + if (not self._closed and key not in self._todo_context + and self._todo_context_bytes + size <= MAX_REQUEST_BYTES): + self._todo_context[key] = result + self._todo_context_bytes += size _REQUEST_RUNTIME_REVISION: ContextVar[_RequestRuntimeRevision | None] = ( @@ -1581,13 +1600,36 @@ def effect_runtime_result( retry_safe: bool = True, large_local_snapshot: bool = False, ) -> Any: - return effect_runtime_request( + state = _REQUEST_RUNTIME_REVISION.get() if method == "todo.context.page" else None + key = None + if state is not None: + # Preserve JSON order and scalar types, including bool/int, signed zero + # and exact integers. Do not infer equality from a digest or Todo id. + try: + key = json.dumps([params, timeout, retry_safe, large_local_snapshot], + ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8") + except (TypeError, ValueError, OverflowError, UnicodeError, RecursionError): + pass # Invalid/non-JSON input still reaches the original validator. + if key is not None: + prior = state.todo_context_result(key) + if prior is not None: + return json.loads(prior) # A caller cannot mutate another view. + result = effect_runtime_request( method, params, timeout=timeout, retry_safe=retry_safe, large_local_snapshot=large_local_snapshot, ).get("result") + if state is not None and key is not None and isinstance(result, dict): + try: + encoded = json.dumps(result, ensure_ascii=False, allow_nan=False, + separators=(",", ":")).encode("utf-8") + except (TypeError, ValueError, OverflowError, UnicodeError, RecursionError): + pass + else: + state.remember_todo_context(key, encoded) + return result def effect_runtime_projection_result(method: str, params: Mapping[str, Any]) -> Any: diff --git a/tests/control_plane/test_effect_runtime_request_scope.py b/tests/control_plane/test_effect_runtime_request_scope.py index 7cd401d59c..c31f4f3edd 100644 --- a/tests/control_plane/test_effect_runtime_request_scope.py +++ b/tests/control_plane/test_effect_runtime_request_scope.py @@ -291,3 +291,80 @@ def collect(**_kwargs: object) -> dict[str, object]: assert payload == {"first": "revision-1", "second": "revision-1"} assert fingerprints == ["revision-1"] + + +def test_repeated_todo_lenses_share_only_exact_request_inputs(monkeypatch): + calls = [] + + def request(method, params, **kwargs): + calls.append((method, params)) + return {"result": {"items": [{"text": params["text"]}]}} + + monkeypatch.setattr(effect_runtime, "effect_runtime_request", request) + with effect_runtime.effect_runtime_request_scope(): + first = effect_runtime.effect_runtime_result("todo.context.page", {"text": "original"}) + first["items"][0]["text"] = "caller edit" + assert effect_runtime.effect_runtime_result("todo.context.page", {"text": "original"}) == { + "items": [{"text": "original"}]} + assert effect_runtime.effect_runtime_result("todo.context.page", {"text": "revised"}) == { + "items": [{"text": "revised"}]} + # JSON equality must not use Python's bool/int equality or lose -0.0. + for value in (True, 1, 1.0, 0.0, -0.0, 2**60, 2**60 + 1): + effect_runtime.effect_runtime_result("todo.context.page", {"text": value}) + assert len(calls) == 9 + + +def test_todo_reuse_ends_with_request_even_for_a_copied_context(monkeypatch): + calls = [] + + def request(method, params, **kwargs): + calls.append(method) + return {"result": {"observed": len(calls)}} + + monkeypatch.setattr(effect_runtime, "effect_runtime_request", request) + with effect_runtime.effect_runtime_request_scope(): + first = effect_runtime.effect_runtime_result("todo.context.page", {}) + inherited = copy_context() + assert inherited.run(effect_runtime.effect_runtime_result, "todo.context.page", {}) == first + assert inherited.run(effect_runtime.effect_runtime_result, "todo.context.page", {}) != first + with effect_runtime.effect_runtime_request_scope(): + assert effect_runtime.effect_runtime_result("todo.context.page", {}) != first + assert len(calls) == 3 + + +def test_authority_reads_settlement_and_failed_lenses_are_never_reused(monkeypatch): + calls = [] + + def request(method, params, **kwargs): + calls.append(method) + if method == "todo.context.page" and calls.count(method) == 1: + raise effect_runtime.EffectRuntimeRejected("invalid fixture") + return {"result": {"observed": len(calls)}} + + monkeypatch.setattr(effect_runtime, "effect_runtime_request", request) + with effect_runtime.effect_runtime_request_scope(): + for method in ("coordination.local_authority.todo_snapshot", "quota.settlement.read", "quota.spend.commit"): + assert effect_runtime.effect_runtime_result(method, {}) != effect_runtime.effect_runtime_result(method, {}) + with pytest.raises(effect_runtime.EffectRuntimeRejected): + effect_runtime.effect_runtime_result("todo.context.page", {}) + assert effect_runtime.effect_runtime_result("todo.context.page", {}) == {"observed": 8} + assert effect_runtime.effect_runtime_result("todo.context.page", {}) == {"observed": 8} + assert len(calls) == 8 + + +def test_todo_reuse_is_bounded_and_unscoped_calls_keep_original_behavior(monkeypatch): + calls = [] + + def request(method, params, **kwargs): + calls.append(method) + return {"result": {"text": "preserve all original output"}} + + monkeypatch.setattr(effect_runtime, "effect_runtime_request", request) + monkeypatch.setattr(effect_runtime, "MAX_REQUEST_BYTES", 1) + with effect_runtime.effect_runtime_request_scope(): + for _ in range(2): + assert effect_runtime.effect_runtime_result("todo.context.page", {}) == { + "text": "preserve all original output"} + for _ in range(2): + effect_runtime.effect_runtime_result("todo.context.page", {}) + assert len(calls) == 4