Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions codeclone/memory/retrieval/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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

Expand Down
19 changes: 0 additions & 19 deletions codeclone/surfaces/mcp/_context_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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],
*,
Expand Down
89 changes: 76 additions & 13 deletions codeclone/surfaces/mcp/_session_memory_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
query_engineering_memory,
)
from ...memory.retrieval.continuation import (
build_memory_continuation_cursor,
memory_projection_request_digest,
rebase_memory_continuation_cursor,
)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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 (
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -962,21 +1007,38 @@ 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:
shown_count = shown[lane]
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",
Expand Down Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion codeclone/surfaces/mcp/_session_state_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions codeclone/surfaces/mcp/messages/help_topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 2 additions & 0 deletions docs/internal/contracts/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<lane>.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.
Expand Down
4 changes: 3 additions & 1 deletion tests/test_mcp_context_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
attach_start_context_governance,
context_governance_digest,
estimate_response_context_units,
passive_drill_down_reachability,
)


Expand Down Expand Up @@ -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"],
Expand Down
Loading