From fbe4d0592edcfe8c4c4cfc1ab50f09b12a13c20d Mon Sep 17 00:00:00 2001 From: Danny Ward Date: Fri, 23 Oct 2026 04:08:28 -0400 Subject: [PATCH] fix(mcp): preserve memory under compact context budgets --- CHANGELOG.md | 5 + README.md | 4 + codeclone/memory/retrieval/service.py | 2 + codeclone/surfaces/mcp/_context_governance.py | 19 --- .../surfaces/mcp/_session_memory_mixin.py | 89 +++++++++-- .../surfaces/mcp/_session_state_mixin.py | 7 +- .../surfaces/mcp/messages/help_topics.py | 4 + docs/internal/contracts/mcp-tools.md | 2 + docs/reference/mcp-tools.md | 1 + tests/test_mcp_context_governance.py | 4 +- tests/test_mcp_memory_management.py | 72 ++++++++- tests/test_mcp_service.py | 150 +++++++++++++++++- 12 files changed, 316 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc52d6d..ac91f950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,11 @@ evidence, platform self-observability, and broader IDE/agent integration. - LCOM4 excludes Protocol methods and Pydantic validation/serialization hooks; `computed_field` remains included. - Repository coverage is enforced at **>=99%**. +### Fixed + +- Compact MCP memory responses now reduce complete lanes before emptying a capped lane, preserve continuations for + partially reduced lanes, avoid duplicating page cursors, and keep static drill-down routes in the help topic. + ### Performance - Relationship facts are collected during the primary module walk instead of a second traversal (cold-cache diff --git a/README.md b/README.md index 986ab34c..f9e42e0b 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,10 @@ codeclone memory search "baseline schema" --match all Retrieval is hybrid — FTS5/BM25 lexical search, optional LanceDB vector search, and Reciprocal Rank Fusion combining the two — with fully reproducible ranking. +Compact MCP retrieval budgets the whole response across records, trajectories, and experiences. Reduced lanes with +visible items keep a digest-bound continuation cursor; if no item fits, the tool returns a contract error instead of an +empty success. + Memory can guide an agent. It cannot authorize edits, override blast radius, change a gate, or replace canonical report facts. diff --git a/codeclone/memory/retrieval/service.py b/codeclone/memory/retrieval/service.py index af737f97..de240ede 100644 --- a/codeclone/memory/retrieval/service.py +++ b/codeclone/memory/retrieval/service.py @@ -683,6 +683,7 @@ def get_relevant_memory( include_drafts: bool = False, include_routine: bool = False, detail_level: str = "compact", + _include_projection_request: bool = False, ) -> dict[str, object]: normalized_detail = _normalize_detail_level(detail_level) raw_scope = scope_paths or () @@ -827,6 +828,7 @@ def get_relevant_memory( ) if continuation: payload["continuation"] = continuation + if continuation or _include_projection_request: payload["_memory_projection_request"] = projection_request return payload diff --git a/codeclone/surfaces/mcp/_context_governance.py b/codeclone/surfaces/mcp/_context_governance.py index f358b01d..338d9a05 100644 --- a/codeclone/surfaces/mcp/_context_governance.py +++ b/codeclone/surfaces/mcp/_context_governance.py @@ -313,7 +313,6 @@ def _attach_context_governance( "enforcement": dict(enforcement), "enforcement_blocked": passive_enforcement_blockers(), "capabilities": passive_context_capabilities(), - "drill_down": passive_drill_down_reachability(), } _attach_omission_context(result, context_governance, evidence_omitted) context_governance.update(response_context) @@ -416,11 +415,6 @@ def _continuation_lane( "page_size", ), ) - cursor_path = str(drill_down.get("cursor_path", "")).strip() - if cursor_path: - cursor = _resolve_dotted_path(payload, cursor_path) - if cursor not in (None, ""): - lane_payload["cursor"] = cursor return lane_payload @@ -442,19 +436,6 @@ def _as_mapping_or_none(value: object) -> Mapping[str, object] | None: return None -def _resolve_dotted_path(payload: Mapping[str, object], dotted_path: str) -> object: - current: object = payload - for raw_part in dotted_path.split("."): - part = raw_part.strip() - if not part: - return None - if isinstance(current, Mapping): - current = current.get(part) - continue - return None - return current - - def attach_finish_context_governance( payload: Mapping[str, object], *, diff --git a/codeclone/surfaces/mcp/_session_memory_mixin.py b/codeclone/surfaces/mcp/_session_memory_mixin.py index 50525cba..51273e5e 100644 --- a/codeclone/surfaces/mcp/_session_memory_mixin.py +++ b/codeclone/surfaces/mcp/_session_memory_mixin.py @@ -36,6 +36,7 @@ query_engineering_memory, ) from ...memory.retrieval.continuation import ( + build_memory_continuation_cursor, memory_projection_request_digest, rebase_memory_continuation_cursor, ) @@ -128,16 +129,31 @@ def get_relevant_memory( include_drafts=effective_include_drafts, include_routine=include_routine, detail_level=detail_level, + _include_projection_request=True, ) if memory_sync is not None: result = dict(result) result["memory_sync"] = memory_sync + continuation_request = result.get("_memory_projection_request") result = self._publish_memory_continuation_request(result) - return _attach_budgeted_memory_retrieval_context( + governed = _attach_budgeted_memory_retrieval_context( result, detail_level=detail_level, max_records=max_records, + continuation_request=( + continuation_request + if is_record_mapping(continuation_request) + else None + ), ) + if is_record_mapping(continuation_request) and _memory_continuation_lanes( + governed + ): + self._remember_memory_continuation_request( + governed.get("project_id"), + continuation_request, + ) + return governed except MemoryContractError as exc: raise MCPServiceContractError(str(exc)) from exc finally: @@ -764,18 +780,23 @@ def _publish_memory_continuation_request( payload: Mapping[str, object], ) -> dict[str, object]: published = dict(payload) - internal = published.pop("_memory_projection_request", None) - project_id = published.get("project_id") - if isinstance(project_id, str) and is_record_mapping(internal): - internal_mapping = internal - digest = memory_projection_request_digest(internal_mapping) - digest_value = digest.get("value") - if isinstance(digest_value, str) and is_payload_dict(internal_mapping): - self._memory_continuation_requests[ - self._memory_continuation_request_key(project_id, digest_value) - ] = dict(internal_mapping) + published.pop("_memory_projection_request", None) return published + def _remember_memory_continuation_request( + self, + project_id: object, + request: Mapping[str, object], + ) -> None: + if not isinstance(project_id, str) or not is_payload_dict(request): + return + digest = memory_projection_request_digest(request) + digest_value = digest.get("value") + if isinstance(digest_value, str): + self._memory_continuation_requests[ + self._memory_continuation_request_key(project_id, digest_value) + ] = dict(request) + def _resolve_memory_continuation_request( self, project_id: str, @@ -795,6 +816,7 @@ def _attach_budgeted_memory_retrieval_context( *, detail_level: str, max_records: int, + continuation_request: Mapping[str, object] | None = None, limit: int | None = None, ) -> dict[str, object]: effective_limit = DEFAULT_RESPONSE_CONTEXT_UNIT_LIMIT if limit is None else limit @@ -806,19 +828,29 @@ def _attach_budgeted_memory_retrieval_context( max_records=max_records, limit=effective_limit, ) + had_memory_items = any(_memory_lane_items(payload).values()) packed, omitted = _pack_compact_memory_response( payload, detail_level=detail_level, max_records=max_records, limit=effective_limit, + continuation_request=continuation_request, ) - return attach_memory_retrieval_context_governance( + governed = attach_memory_retrieval_context_governance( packed, detail_level=detail_level, max_records=max_records, evidence_omitted=omitted, limit=effective_limit, ) + if had_memory_items and not any(_memory_lane_items(packed).values()): + envelope = cast("dict[str, object]", governed["context_governance"]) + raise MCPServiceContractError( + "get_relevant_memory could not fit any memory items in the response " + f"budget (empty-response estimate: {envelope['estimated']} units; " + f"limit: {effective_limit} units)." + ) + return governed def _pack_compact_memory_response( @@ -827,18 +859,24 @@ def _pack_compact_memory_response( detail_level: str, max_records: int, limit: int, + continuation_request: Mapping[str, object] | None = None, ) -> tuple[dict[str, object], dict[str, object] | None]: lane_items = _memory_lane_items(payload) totals = _memory_lane_totals(payload, lane_items) original_shown = {lane: len(items) for lane, items in lane_items.items()} shown = dict(original_shown) original_continuation = _memory_continuation_lanes(payload) + project_id = payload.get("project_id") + can_create_continuation = ( + isinstance(project_id, str) and continuation_request is not None + ) packed = _memory_response_with_shown_counts( payload, lane_items=lane_items, totals=totals, shown=shown, original_continuation=original_continuation, + continuation_request=continuation_request, ) omitted = _memory_governance_omitted(packed, original_shown=original_shown) if ( @@ -858,6 +896,7 @@ def _pack_compact_memory_response( shown, floor=floor, original_continuation=original_continuation, + allow_complete=can_create_continuation, ) if lane is None: break @@ -868,6 +907,7 @@ def _pack_compact_memory_response( totals=totals, shown=shown, original_continuation=original_continuation, + continuation_request=continuation_request, ) omitted = _memory_governance_omitted( packed, @@ -940,6 +980,7 @@ def _memory_response_with_shown_counts( totals: Mapping[str, int], shown: Mapping[str, int], original_continuation: Mapping[str, Mapping[str, object]], + continuation_request: Mapping[str, object] | None = None, ) -> dict[str, object]: result = dict(payload) for lane, count_key in _MEMORY_RESPONSE_LANES: @@ -948,10 +989,14 @@ def _memory_response_with_shown_counts( result[count_key] = shown_count result["truncated"] = totals["records"] > shown["records"] result["trajectories_truncated"] = totals["trajectories"] > shown["trajectories"] + result["experiences_truncated"] = totals["experiences"] > shown["experiences"] continuation = _rebuilt_memory_continuation( + project_id=payload.get("project_id"), + lane_items=lane_items, totals=totals, shown=shown, original_continuation=original_continuation, + continuation_request=continuation_request, ) if continuation: result["continuation"] = continuation @@ -962,9 +1007,12 @@ def _memory_response_with_shown_counts( def _rebuilt_memory_continuation( *, + project_id: object, + lane_items: Mapping[str, Sequence[Mapping[str, object]]], totals: Mapping[str, int], shown: Mapping[str, int], original_continuation: Mapping[str, Mapping[str, object]], + continuation_request: Mapping[str, object] | None = None, ) -> dict[str, object]: lanes: dict[str, object] = {} for lane, _count_key in _MEMORY_RESPONSE_LANES: @@ -972,11 +1020,25 @@ def _rebuilt_memory_continuation( total = totals[lane] omitted = max(0, total - shown_count) if omitted > 0: + if shown_count == 0: + continue page = _rebased_memory_lane_page( lane, offset=shown_count, original_continuation=original_continuation, ) + if ( + page is None + and isinstance(project_id, str) + and continuation_request is not None + ): + page = build_memory_continuation_cursor( + project_id=project_id, + lane=lane, + request=continuation_request, + items=lane_items[lane], + offset=shown_count, + ) if page is not None: lanes[lane] = { "status": "available", @@ -1062,9 +1124,10 @@ def _next_reducible_memory_lane( *, floor: int, original_continuation: Mapping[str, Mapping[str, object]], + allow_complete: bool = False, ) -> str | None: for lane in _MEMORY_RESPONSE_REDUCTION_ORDER: - if shown[lane] > floor and lane in original_continuation: + if shown[lane] > floor and (allow_complete or lane in original_continuation): return lane return None diff --git a/codeclone/surfaces/mcp/_session_state_mixin.py b/codeclone/surfaces/mcp/_session_state_mixin.py index 343a01e9..0b9d5f61 100644 --- a/codeclone/surfaces/mcp/_session_state_mixin.py +++ b/codeclone/surfaces/mcp/_session_state_mixin.py @@ -9,7 +9,10 @@ from ...baseline.metrics_baseline import probe_metrics_baseline_section from . import _session_helpers as _helpers from ._blast_radius import BlastRadiusResult -from ._context_governance import attach_passive_context_governance +from ._context_governance import ( + attach_passive_context_governance, + passive_drill_down_reachability, +) from ._implementation_context_pages import ContextProjectionArtifact from ._intent import IntentRecord from ._report_section import ( @@ -1212,6 +1215,8 @@ def get_help( {"title": title, "url": url} for title, url in spec.doc_links ], } + if validated_topic == "engineering_memory": + payload["drill_down"] = passive_drill_down_reachability() if spec.anti_patterns: payload["anti_patterns"] = list(spec.anti_patterns) if validated_detail == "normal" and spec.warnings: diff --git a/codeclone/surfaces/mcp/messages/help_topics.py b/codeclone/surfaces/mcp/messages/help_topics.py index de3cd000..02092ecd 100644 --- a/codeclone/surfaces/mcp/messages/help_topics.py +++ b/codeclone/surfaces/mcp/messages/help_topics.py @@ -769,6 +769,10 @@ class MCPHelpTopicSpec: "trajectories, and experiences point to digest-bound " "get_memory_projection_page cursors." ), + ( + "Static drill-down reachability is returned in this topic's " + "drill_down field, not copied into each context_governance envelope." + ), ( "Semantic (off by default): enable sidecar, rebuild_semantic_index, " "then search with semantic=true." diff --git a/docs/internal/contracts/mcp-tools.md b/docs/internal/contracts/mcp-tools.md index 1b1d7384..36705850 100644 --- a/docs/internal/contracts/mcp-tools.md +++ b/docs/internal/contracts/mcp-tools.md @@ -73,6 +73,8 @@ Memory-aware tools require `get_relevant_memory` after `start_controlled_change` - Do not treat `draft`, `inferred`, or excluded stale records as established facts - If a `contradiction_note` exists for your scope, surface it before editing +Compact memory retrieval budgets all lanes and governance metadata as one response. It may reduce complete lanes as well as lanes that already have a continuation; a reduced lane with visible items receives a digest-bound cursor, while an empty lane has no cursor. The `_continuation` pointer exposes the cursor path instead of copying the token. Static drill-down routes are available through `help(topic='engineering_memory')`, rather than in every governance envelope. If no memory item fits the limit, retrieval returns a contract error. + ## Failure modes ### Concurrent intent collision diff --git a/docs/reference/mcp-tools.md b/docs/reference/mcp-tools.md index 91f4911a..b4f0e750 100644 --- a/docs/reference/mcp-tools.md +++ b/docs/reference/mcp-tools.md @@ -105,6 +105,7 @@ Return actionable remediation guidance for a single finding. Returns `status="no **`get_relevant_memory(root, scope, intent_id, ...)`** Return ranked, evidence-linked engineering memory for the declared edit scope. Read-only; does not mutate the memory database. +Compact responses budget records, trajectories, experiences, and governance metadata together. A reduced lane with visible items has a digest-bound continuation at `continuation.lanes..page.cursor`; the `_continuation` pointer names its `cursor_path` without copying the cursor. Static drill-down routes are available through `help(topic='engineering_memory')`, rather than in every governance envelope. If no memory item fits, the tool returns a contract error rather than an empty success. **`manage_engineering_memory(root, action, ...)`** Engineering memory governance for agents. Actions: `refresh_from_run`, `record_candidate`, `promote_experience`, `validate_claims`, `propose_from_receipt`, `rebuild_semantic_index`, `rebuild_trajectories`, `enqueue_projection_rebuild`, `projection_rebuild_status`, `run_projection_jobs_once`. Approve, reject, and archive are not available to agents. diff --git a/tests/test_mcp_context_governance.py b/tests/test_mcp_context_governance.py index 73cc3fa1..e2c50564 100644 --- a/tests/test_mcp_context_governance.py +++ b/tests/test_mcp_context_governance.py @@ -29,6 +29,7 @@ attach_start_context_governance, context_governance_digest, estimate_response_context_units, + passive_drill_down_reachability, ) @@ -320,7 +321,8 @@ def test_implementation_context_governance_enforces_compact_budget() -> None: def test_context_governance_declares_drill_down_reachability() -> None: payload = attach_passive_context_governance({"status": "accepted"}) envelope = cast("dict[str, object]", payload["context_governance"]) - drill_down = cast("dict[str, dict[str, object]]", envelope["drill_down"]) + assert "drill_down" not in envelope + drill_down = passive_drill_down_reachability() assert { "memory_record_lookup": drill_down["memory_record"]["object_lookup"], diff --git a/tests/test_mcp_memory_management.py b/tests/test_mcp_memory_management.py index 82e1916b..fb70c6d1 100644 --- a/tests/test_mcp_memory_management.py +++ b/tests/test_mcp_memory_management.py @@ -471,7 +471,7 @@ def test_mcp_get_relevant_memory_compact_enforces_response_budget( monkeypatch.setattr( mcp_memory_mixin_mod, "DEFAULT_RESPONSE_CONTEXT_UNIT_LIMIT", - 1700, + 1400, ) with cli_memory_repo(tmp_path, with_draft=False) as (root, project, store): for index in range(8): @@ -508,6 +508,76 @@ def test_mcp_get_relevant_memory_compact_enforces_response_budget( assert page["offset"] == record_count +def test_mcp_budget_can_reduce_complete_lanes_and_remember_page_request( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + from codeclone.memory.retrieval.continuation import ( + memory_projection_request_digest, + ) + + request: dict[str, object] = { + "scope_paths": ["pkg/mod.py"], + "symbols": [], + "blast_dependents": [], + "scope_resolved_from": "explicit", + "include_stale": False, + "include_drafts": False, + "include_routine": False, + "detail_level": "compact", + } + records = [ + {"id": f"rec-{index}", "statement": f"record {index}"} for index in range(7) + ] + experiences = [ + {"id": f"exp-{index}", "summary": "context " * 200} for index in range(8) + ] + + def get_complete_memory( + *_args: object, + project_id: str, + **_kwargs: object, + ) -> dict[str, object]: + return { + "project_id": project_id, + "scope_resolved_from": "explicit", + "records": records, + "record_count": len(records), + "trajectories": [], + "trajectory_count": 0, + "experiences": experiences, + "experience_count": len(experiences), + "truncated": False, + "trajectories_truncated": False, + "coverage": {}, + "detail_level": "compact", + "retrieval_policy": {}, + "_memory_projection_request": request, + } + + monkeypatch.setattr( + mcp_memory_mixin_mod, + "get_relevant_memory", + get_complete_memory, + ) + with cli_memory_repo(tmp_path, with_draft=False) as (root, project, _store): + service = CodeCloneMCPService(history_limit=2) + payload = service.get_relevant_memory( + root=str(root.resolve()), + scope=["pkg/mod.py"], + max_records=20, + ) + + assert 0 < cast("int", payload["experience_count"]) < len(experiences) + experience_lane = _nested_dict(payload, "continuation", "lanes", "experiences") + page = cast("dict[str, object]", experience_lane["page"]) + cursor = str(page["cursor"]) + digest = memory_projection_request_digest(request) + key = f"{project.id}:{digest['value']}" + assert service._memory_continuation_requests[key] == request + assert cursor + + def _nested_dict(payload: dict[str, object], *keys: str) -> dict[str, object]: current: object = payload for key in keys: diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index 7080c8fa..095231fc 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -2790,9 +2790,10 @@ def test_mcp_context_governance_adds_top_level_continuation_pointer() -> None: "tool": "get_memory_projection_page", "cursor_path": "continuation.lanes.records.page.cursor", "snapshot_identity": "memory projection cursor", - "cursor": "cursor-records-1", } ] + governance = cast("dict[str, object]", payload["context_governance"]) + assert "drill_down" not in governance def test_mcp_service_help_overview_returns_topic_index() -> None: @@ -2943,6 +2944,10 @@ def test_mcp_service_help_validates_topic_and_detail() -> None: service.get_help(topic="baseline", detail="full") memory_help = service.get_help(topic="engineering_memory", detail="normal") + drill_down = cast("dict[str, dict[str, object]]", memory_help["drill_down"]) + assert drill_down["memory_record"]["continuation_route"] == ( + "get_memory_projection_page(cursor=...)" + ) assert "get_relevant_memory" in cast( "list[str]", memory_help["recommended_tools"], @@ -14483,6 +14488,140 @@ def test_pack_compact_memory_response_shrinks_lanes_to_fit_budget() -> None: assert cast("int", packed["record_count"]) < len(records) +def test_pack_compact_memory_response_reduces_complete_lane_and_keeps_one_item() -> ( + None +): + import codeclone.surfaces.mcp._session_memory_mixin as mem_mod + from codeclone.memory.retrieval.continuation import ( + build_memory_continuation_cursor, + decode_memory_continuation_cursor, + ) + + request: dict[str, object] = { + "scope_paths": ["pkg/service.py"], + "symbols": [], + "blast_dependents": [], + "scope_resolved_from": "explicit", + "include_stale": False, + "include_drafts": False, + "include_routine": False, + "detail_level": "compact", + } + records = [ + {"id": f"rec-{index}", "statement": f"record {index}"} for index in range(7) + ] + experiences = [ + { + "id": f"exp-{index}", + "summary": f"experience {index} " + "context " * 200, + } + for index in range(8) + ] + record_page = build_memory_continuation_cursor( + project_id="project-1", + lane="records", + request=request, + items=records, + offset=1, + ) + payload: dict[str, object] = { + "project_id": "project-1", + "scope_resolved_from": "explicit", + "records": records[:1], + "record_count": 1, + "trajectories": [], + "trajectory_count": 0, + "experiences": experiences, + "experience_count": len(experiences), + "truncated": True, + "trajectories_truncated": False, + "coverage": {}, + "detail_level": "compact", + "retrieval_policy": {}, + "continuation": { + "projection_kind": "memory_retrieval_lane_projection_v1", + "ordering_version": "memory_retrieval_lane_order_v1", + "cursor_policy": "digest_bound_recompute_or_fail_closed", + "lanes": { + "records": { + "status": "available", + "total": len(records), + "shown": 1, + "omitted": len(records) - 1, + "page": record_page, + } + }, + }, + } + + packed, omitted = mem_mod._pack_compact_memory_response( + payload, + detail_level="compact", + max_records=1, + limit=2200, + continuation_request=request, + ) + governed = mcp_context_governance_mod.attach_memory_retrieval_context_governance( + packed, + detail_level="compact", + max_records=1, + evidence_omitted=omitted, + limit=2200, + ) + envelope = cast("dict[str, object]", governed["context_governance"]) + continuation = cast("dict[str, object]", governed["continuation"]) + experience_lane = cast( + "dict[str, object]", + cast("dict[str, object]", continuation["lanes"])["experiences"], + ) + page = cast("dict[str, object]", experience_lane["page"]) + cursor = decode_memory_continuation_cursor(str(page["cursor"])) + + assert governed["record_count"] == 1 + assert 0 < cast("int", governed["experience_count"]) < len(experiences) + assert governed["experiences_truncated"] is True + estimate = envelope["estimated"] + limit_value = envelope["limit"] + assert isinstance(estimate, int) and isinstance(limit_value, int) + assert estimate <= limit_value + assert envelope["mandatory_overflow"] is False + assert cursor["lane"] == "experiences" + assert cursor["offset"] == governed["experience_count"] + + +def test_compact_memory_budget_refuses_when_no_item_can_fit() -> None: + import codeclone.surfaces.mcp._session_memory_mixin as mem_mod + + payload: dict[str, object] = { + "project_id": "project-1", + "records": [{"id": "rec-1", "statement": "one record"}], + "record_count": 1, + "trajectories": [], + "trajectory_count": 0, + "experiences": [], + "experience_count": 0, + } + request = {"scope_paths": ["pkg/service.py"]} + packed, _omitted = mem_mod._pack_compact_memory_response( + payload, + detail_level="compact", + max_records=1, + limit=0, + continuation_request=request, + ) + + assert packed["records"] == [] + assert "continuation" not in packed + with pytest.raises(MCPServiceContractError, match="could not fit any memory items"): + mem_mod._attach_budgeted_memory_retrieval_context( + payload, + detail_level="compact", + max_records=1, + limit=0, + continuation_request=request, + ) + + def test_memory_governed_estimate_rejects_non_integer_estimate( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -14664,12 +14803,7 @@ def test_context_governance_helper_edges() -> None: }, ) assert lane is not None - assert lane["cursor"] == "lane-2" - - assert mcp_context_governance_mod._resolve_dotted_path({}, "..bad") is None - assert ( - mcp_context_governance_mod._resolve_dotted_path({"items": []}, "items.id") - is None - ) + assert lane["cursor_path"] == "nested.cursor" + assert "cursor" not in lane assert mcp_context_governance_mod._continuation_payload({}, {}) is None