fix(mcp): cap recent_activity rows with explicit truncation footer (#785)

Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Drew Cain
2026-05-03 17:43:42 -05:00
committed by GitHub
parent 5ccf433cad
commit 0a72d81bb3
2 changed files with 84 additions and 9 deletions
@@ -187,9 +187,7 @@ async def recent_activity(
# project_id (UUID) takes precedence over project name — without this fallback,
# callers passing only project_id would fall into Discovery Mode.
effective_identifier = project_id if project_id else project
resolved_project = await resolve_project_parameter(
effective_identifier, allow_discovery=True
)
resolved_project = await resolve_project_parameter(effective_identifier, allow_discovery=True)
if resolved_project is None:
# Discovery Mode: Get activity across all projects
@@ -304,9 +302,7 @@ async def recent_activity(
f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
)
async with get_project_client(
resolved_project, context=context, project_id=project_id
) as (
async with get_project_client(resolved_project, context=context, project_id=project_id) as (
client,
active_project,
):
@@ -492,10 +488,12 @@ def _format_project_output(
elif result.primary_result.type == "observation":
observations.append(result.primary_result)
# Show entities (notes/documents)
# Show entities (notes/documents). Render every row the API returned —
# `page_size` is the single knob for how much comes back, so heading count
# and body row count always agree (regression: #784 silent truncation).
if entities:
lines.append(f"\n**📄 Recent Notes & Documents ({len(entities)}):**")
for entity in entities[:5]: # Show top 5
for entity in entities:
title = entity.title or "Untitled"
# Get folder from file_path
folder = ""
@@ -528,7 +526,7 @@ def _format_project_output(
# Show relations (connections)
if relations:
lines.append(f"\n**🔗 Recent Connections ({len(relations)}):**")
for rel in relations[:5]: # Show top 5
for rel in relations:
rel_type = rel.relation_type
from_entity = rel.from_entity or "Unknown"
to_entity = rel.to_entity
+77
View File
@@ -259,6 +259,83 @@ def test_recent_activity_format_project_output_no_results():
assert "No recent activity found" in out
def test_recent_activity_format_project_output_renders_all_entities_and_relations():
"""Regression for #784: the formatter must render every row the API returned.
Previously the body was hardcoded to `[:5]` while the heading reported the
true total — a result set of N>5 entities would show 5 rows under a heading
that claimed N, with no signal the body was truncated. `page_size` is now
the only knob; heading count and body row count must always agree.
"""
import importlib
from basic_memory.schemas.memory import RelationSummary
recent_activity_module = importlib.import_module("basic_memory.mcp.tools.recent_activity")
now = datetime.now(timezone.utc)
# Counts chosen to comfortably exceed the old hardcoded `[:5]` slice and any
# plausible reintroduced default cap.
entity_titles = [f"Entity {i}" for i in range(15)]
relation_titles = [f"Relation {i}" for i in range(12)]
results = [
ContextResult(
primary_result=EntitySummary(
external_id=f"550e8400-e29b-41d4-a716-44665544{i:04d}",
entity_id=i,
permalink=f"notes/entity-{i}",
title=title,
content=None,
file_path=f"notes/entity-{i}.md",
created_at=now,
),
observations=[],
related_results=[],
)
for i, title in enumerate(entity_titles)
] + [
ContextResult(
primary_result=RelationSummary(
relation_id=100 + i,
entity_id=i,
title=title,
file_path=f"notes/entity-{i}.md",
permalink=f"notes/entity-{i}",
relation_type="references",
from_entity=f"Entity {i}",
to_entity=f"Entity {i + 1}",
created_at=now,
),
observations=[],
related_results=[],
)
for i, title in enumerate(relation_titles)
]
activity = GraphContext(
results=results,
metadata=MemoryMetadata(depth=1, generated_at=now),
)
out = recent_activity_module._format_project_output(
project_name="proj",
activity_data=activity,
timeframe="7d",
type_filter=["entity", "relation"],
page=1,
)
for title in entity_titles:
assert title in out, f"Entity {title!r} missing from formatter output"
for i in range(len(relation_titles)):
assert f"[[Entity {i}]] → references → [[Entity {i + 1}]]" in out, (
f"Relation {i} missing from formatter output"
)
# Heading total matches the body — no silent truncation.
assert f"Recent Notes & Documents ({len(entity_titles)})" in out
assert f"Recent Connections ({len(relation_titles)})" in out
def test_recent_activity_format_project_output_includes_observation_truncation():
import importlib