Skip to content

Commit 0a72d81

Browse files
groksrcclaude
andauthored
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>
1 parent 5ccf433 commit 0a72d81

2 files changed

Lines changed: 84 additions & 9 deletions

File tree

src/basic_memory/mcp/tools/recent_activity.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ async def recent_activity(
187187
# project_id (UUID) takes precedence over project name — without this fallback,
188188
# callers passing only project_id would fall into Discovery Mode.
189189
effective_identifier = project_id if project_id else project
190-
resolved_project = await resolve_project_parameter(
191-
effective_identifier, allow_discovery=True
192-
)
190+
resolved_project = await resolve_project_parameter(effective_identifier, allow_discovery=True)
193191

194192
if resolved_project is None:
195193
# Discovery Mode: Get activity across all projects
@@ -304,9 +302,7 @@ async def recent_activity(
304302
f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
305303
)
306304

307-
async with get_project_client(
308-
resolved_project, context=context, project_id=project_id
309-
) as (
305+
async with get_project_client(resolved_project, context=context, project_id=project_id) as (
310306
client,
311307
active_project,
312308
):
@@ -492,10 +488,12 @@ def _format_project_output(
492488
elif result.primary_result.type == "observation":
493489
observations.append(result.primary_result)
494490

495-
# Show entities (notes/documents)
491+
# Show entities (notes/documents). Render every row the API returned —
492+
# `page_size` is the single knob for how much comes back, so heading count
493+
# and body row count always agree (regression: #784 silent truncation).
496494
if entities:
497495
lines.append(f"\n**📄 Recent Notes & Documents ({len(entities)}):**")
498-
for entity in entities[:5]: # Show top 5
496+
for entity in entities:
499497
title = entity.title or "Untitled"
500498
# Get folder from file_path
501499
folder = ""
@@ -528,7 +526,7 @@ def _format_project_output(
528526
# Show relations (connections)
529527
if relations:
530528
lines.append(f"\n**🔗 Recent Connections ({len(relations)}):**")
531-
for rel in relations[:5]: # Show top 5
529+
for rel in relations:
532530
rel_type = rel.relation_type
533531
from_entity = rel.from_entity or "Unknown"
534532
to_entity = rel.to_entity

tests/mcp/test_tool_recent_activity.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,83 @@ def test_recent_activity_format_project_output_no_results():
259259
assert "No recent activity found" in out
260260

261261

262+
def test_recent_activity_format_project_output_renders_all_entities_and_relations():
263+
"""Regression for #784: the formatter must render every row the API returned.
264+
Previously the body was hardcoded to `[:5]` while the heading reported the
265+
true total — a result set of N>5 entities would show 5 rows under a heading
266+
that claimed N, with no signal the body was truncated. `page_size` is now
267+
the only knob; heading count and body row count must always agree.
268+
"""
269+
import importlib
270+
271+
from basic_memory.schemas.memory import RelationSummary
272+
273+
recent_activity_module = importlib.import_module("basic_memory.mcp.tools.recent_activity")
274+
now = datetime.now(timezone.utc)
275+
276+
# Counts chosen to comfortably exceed the old hardcoded `[:5]` slice and any
277+
# plausible reintroduced default cap.
278+
entity_titles = [f"Entity {i}" for i in range(15)]
279+
relation_titles = [f"Relation {i}" for i in range(12)]
280+
281+
results = [
282+
ContextResult(
283+
primary_result=EntitySummary(
284+
external_id=f"550e8400-e29b-41d4-a716-44665544{i:04d}",
285+
entity_id=i,
286+
permalink=f"notes/entity-{i}",
287+
title=title,
288+
content=None,
289+
file_path=f"notes/entity-{i}.md",
290+
created_at=now,
291+
),
292+
observations=[],
293+
related_results=[],
294+
)
295+
for i, title in enumerate(entity_titles)
296+
] + [
297+
ContextResult(
298+
primary_result=RelationSummary(
299+
relation_id=100 + i,
300+
entity_id=i,
301+
title=title,
302+
file_path=f"notes/entity-{i}.md",
303+
permalink=f"notes/entity-{i}",
304+
relation_type="references",
305+
from_entity=f"Entity {i}",
306+
to_entity=f"Entity {i + 1}",
307+
created_at=now,
308+
),
309+
observations=[],
310+
related_results=[],
311+
)
312+
for i, title in enumerate(relation_titles)
313+
]
314+
315+
activity = GraphContext(
316+
results=results,
317+
metadata=MemoryMetadata(depth=1, generated_at=now),
318+
)
319+
320+
out = recent_activity_module._format_project_output(
321+
project_name="proj",
322+
activity_data=activity,
323+
timeframe="7d",
324+
type_filter=["entity", "relation"],
325+
page=1,
326+
)
327+
328+
for title in entity_titles:
329+
assert title in out, f"Entity {title!r} missing from formatter output"
330+
for i in range(len(relation_titles)):
331+
assert f"[[Entity {i}]] → references → [[Entity {i + 1}]]" in out, (
332+
f"Relation {i} missing from formatter output"
333+
)
334+
# Heading total matches the body — no silent truncation.
335+
assert f"Recent Notes & Documents ({len(entity_titles)})" in out
336+
assert f"Recent Connections ({len(relation_titles)})" in out
337+
338+
262339
def test_recent_activity_format_project_output_includes_observation_truncation():
263340
import importlib
264341

0 commit comments

Comments
 (0)