Skip to content

Commit 6b348df

Browse files
groksrcclaude
andcommitted
fix(mcp): drop recent_activity display cap; render all returned rows
Address review feedback on #785. Replaces the 10-row display cap (and its truncation footer) with no cap at all — `page_size` is now the single knob controlling how many rows come back, so the heading count and body row count always agree. This is the #784 fix without the two-layer pagination system the first pass introduced. The rejected truncation footer told callers to "raise page_size to display all", which was misleading: `_PROJECT_OUTPUT_DISPLAY_CAP` was display-only, independent of `page_size`, so raising it didn't help. Dropping the cap removes the entire class of confusion. Also drops the test for the removed cap behavior; widens the renders-all test to 15 entities / 12 relations to comfortably exceed the old hardcoded `[:5]` slice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
1 parent ef18845 commit 6b348df

2 files changed

Lines changed: 18 additions & 112 deletions

File tree

src/basic_memory/mcp/tools/recent_activity.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@
2525
from basic_memory.schemas.search import SearchItemType
2626

2727

28-
# Display cap for project-mode entity and relation rows in the formatted output.
29-
# The API has already paginated to `page_size`; this is a separate readability
30-
# guardrail that keeps the LLM-facing text compact when callers ask for large
31-
# pages. When exceeded, `_format_project_output` appends an explicit
32-
# "…and N more" line so the truncation is visible rather than silent.
33-
_PROJECT_OUTPUT_DISPLAY_CAP = 10
34-
35-
3628
@mcp.tool(
3729
description="""Get recent activity for a project or across all projects.
3830
@@ -496,16 +488,12 @@ def _format_project_output(
496488
elif result.primary_result.type == "observation":
497489
observations.append(result.primary_result)
498490

499-
# Show entities (notes/documents).
500-
# Trigger: more entities returned than the display cap.
501-
# Why: keep formatted output compact for LLM context while still surfacing the
502-
# true total in the heading. Previously the cap was silent — the heading said
503-
# N items and the body listed only 5, with no signal the body was truncated.
504-
# Outcome: render up to `_PROJECT_OUTPUT_DISPLAY_CAP` rows; if any were dropped,
505-
# append an explicit "…and N more" line directing the caller to raise page_size.
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).
506494
if entities:
507495
lines.append(f"\n**📄 Recent Notes & Documents ({len(entities)}):**")
508-
for entity in entities[:_PROJECT_OUTPUT_DISPLAY_CAP]:
496+
for entity in entities:
509497
title = entity.title or "Untitled"
510498
# Get folder from file_path
511499
folder = ""
@@ -514,9 +502,6 @@ def _format_project_output(
514502
if folder_path and folder_path != ".":
515503
folder = f" ({folder_path})"
516504
lines.append(f" • {title}{folder}")
517-
if len(entities) > _PROJECT_OUTPUT_DISPLAY_CAP:
518-
hidden = len(entities) - _PROJECT_OUTPUT_DISPLAY_CAP
519-
lines.append(f" …and {hidden} more on this page (raise page_size to display all)")
520505

521506
# Show observations (categorized insights)
522507
if observations:
@@ -538,10 +523,10 @@ def _format_project_output(
538523
content = _truncate_at_word(content, 80)
539524
lines.append(f" - {content}")
540525

541-
# Show relations (connections). Same cap-with-explicit-truncation pattern as entities.
526+
# Show relations (connections)
542527
if relations:
543528
lines.append(f"\n**🔗 Recent Connections ({len(relations)}):**")
544-
for rel in relations[:_PROJECT_OUTPUT_DISPLAY_CAP]:
529+
for rel in relations:
545530
rel_type = rel.relation_type
546531
from_entity = rel.from_entity or "Unknown"
547532
to_entity = rel.to_entity
@@ -551,9 +536,6 @@ def _format_project_output(
551536
to_link = f"[[{to_entity}]]" if to_entity else "[Missing Link]"
552537

553538
lines.append(f" • {from_link}{rel_type}{to_link}")
554-
if len(relations) > _PROJECT_OUTPUT_DISPLAY_CAP:
555-
hidden = len(relations) - _PROJECT_OUTPUT_DISPLAY_CAP
556-
lines.append(f" …and {hidden} more on this page (raise page_size to display all)")
557539

558540
# Activity summary with pagination guidance
559541
total = len(activity_data.results)

tests/mcp/test_tool_recent_activity.py

Lines changed: 12 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,11 @@ def test_recent_activity_format_project_output_no_results():
260260

261261

262262
def test_recent_activity_format_project_output_renders_all_entities_and_relations():
263-
"""Regression for #784, part 1: when the result count is below the display cap
264-
the formatter must render every row. Previously the cap was hardcoded at 5,
265-
so a result set of 7 entities would show 5 rows under a heading that claimed 7.
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.
266268
"""
267269
import importlib
268270

@@ -271,8 +273,10 @@ def test_recent_activity_format_project_output_renders_all_entities_and_relation
271273
recent_activity_module = importlib.import_module("basic_memory.mcp.tools.recent_activity")
272274
now = datetime.now(timezone.utc)
273275

274-
entity_titles = [f"Entity {i}" for i in range(7)]
275-
relation_titles = [f"Relation {i}" for i in range(6)]
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)]
276280

277281
results = [
278282
ContextResult(
@@ -327,89 +331,9 @@ def test_recent_activity_format_project_output_renders_all_entities_and_relation
327331
assert f"[[Entity {i}]] → references → [[Entity {i + 1}]]" in out, (
328332
f"Relation {i} missing from formatter output"
329333
)
330-
# Below-cap path: no truncation footer should appear.
331-
assert "more on this page" not in out
332-
333-
334-
def test_recent_activity_format_project_output_caps_with_explicit_truncation():
335-
"""Regression for #784, part 2: above the display cap the formatter must
336-
truncate to the cap AND emit an explicit "…and N more" footer so the caller
337-
can see that the body is a preview of a longer page (and knows to raise
338-
page_size).
339-
"""
340-
import importlib
341-
342-
from basic_memory.schemas.memory import RelationSummary
343-
344-
recent_activity_module = importlib.import_module("basic_memory.mcp.tools.recent_activity")
345-
cap = recent_activity_module._PROJECT_OUTPUT_DISPLAY_CAP
346-
now = datetime.now(timezone.utc)
347-
348-
entity_count = cap + 2
349-
relation_count = cap + 3
350-
351-
# Use prefixes that don't collide between entities and relation endpoints,
352-
# so substring assertions don't accidentally match across rows.
353-
results = [
354-
ContextResult(
355-
primary_result=EntitySummary(
356-
external_id=f"550e8400-e29b-41d4-a716-44665544{i:04d}",
357-
entity_id=i,
358-
permalink=f"notes/entity-{i}",
359-
title=f"NoteFile {i}",
360-
content=None,
361-
file_path=f"notes/entity-{i}.md",
362-
created_at=now,
363-
),
364-
observations=[],
365-
related_results=[],
366-
)
367-
for i in range(entity_count)
368-
] + [
369-
ContextResult(
370-
primary_result=RelationSummary(
371-
relation_id=1000 + i,
372-
entity_id=i,
373-
title=f"Relation {i}",
374-
file_path=f"notes/entity-{i}.md",
375-
permalink=f"notes/entity-{i}",
376-
relation_type="references",
377-
from_entity=f"FromNode {i}",
378-
to_entity=f"ToNode {i}",
379-
created_at=now,
380-
),
381-
observations=[],
382-
related_results=[],
383-
)
384-
for i in range(relation_count)
385-
]
386-
387-
activity = GraphContext(
388-
results=results,
389-
metadata=MemoryMetadata(depth=1, generated_at=now),
390-
)
391-
392-
out = recent_activity_module._format_project_output(
393-
project_name="proj",
394-
activity_data=activity,
395-
timeframe="7d",
396-
type_filter=["entity", "relation"],
397-
page=1,
398-
)
399-
400-
# Heading reports the true total (independent of truncation).
401-
assert f"Recent Notes & Documents ({entity_count})" in out
402-
assert f"Recent Connections ({relation_count})" in out
403-
404-
# Body shows exactly `cap` rows of each.
405-
assert f"NoteFile {cap - 1}" in out, "expected last in-cap entity to appear"
406-
assert f"NoteFile {cap}" not in out, "entity beyond cap should be truncated"
407-
assert f"[[FromNode {cap - 1}]] → references → [[ToNode {cap - 1}]]" in out
408-
assert f"[[FromNode {cap}]]" not in out, "relation beyond cap should be truncated"
409-
410-
# Truncation footer is present and reports the correct hidden count.
411-
assert f"…and {entity_count - cap} more on this page" in out
412-
assert f"…and {relation_count - cap} more on this page" in out
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
413337

414338

415339
def test_recent_activity_format_project_output_includes_observation_truncation():

0 commit comments

Comments
 (0)