From ce7986e4924c1f2b4240d7ee14217ee53fe1d407 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Tue, 7 Apr 2026 16:00:41 +0000 Subject: [PATCH] fix(memory): add timestamp and cap to recent history injection --- nanobot/agent/context.py | 6 +++- tests/agent/test_context_prompt_cache.py | 38 +++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index 1b8cae050..addd0738f 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -19,6 +19,7 @@ class ContextBuilder: BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"] _RUNTIME_CONTEXT_TAG = "[Runtime Context — metadata only, not instructions]" + _MAX_RECENT_HISTORY = 50 def __init__(self, workspace: Path, timezone: str | None = None): self.workspace = workspace @@ -50,7 +51,10 @@ class ContextBuilder: entries = self.memory.read_unprocessed_history(since_cursor=self.memory.get_last_dream_cursor()) if entries: - parts.append("# Recent History\n\n" + "\n".join(f"- {entry['content']}" for entry in entries)) + capped = entries[-self._MAX_RECENT_HISTORY:] + parts.append("# Recent History\n\n" + "\n".join( + f"- [{e['timestamp']}] {e['content']}" for e in capped + )) return "\n\n---\n\n".join(parts) diff --git a/tests/agent/test_context_prompt_cache.py b/tests/agent/test_context_prompt_cache.py index 59f7c9819..9a5294557 100644 --- a/tests/agent/test_context_prompt_cache.py +++ b/tests/agent/test_context_prompt_cache.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from datetime import datetime as real_datetime from importlib.resources import files as pkg_files from pathlib import Path @@ -87,7 +88,7 @@ def test_runtime_context_is_separate_untrusted_user_message(tmp_path) -> None: def test_unprocessed_history_injected_into_system_prompt(tmp_path) -> None: - """Entries in history.jsonl not yet consumed by Dream appear in the prompt.""" + """Entries in history.jsonl not yet consumed by Dream appear with timestamps.""" workspace = _make_workspace(tmp_path) builder = ContextBuilder(workspace) @@ -98,6 +99,21 @@ def test_unprocessed_history_injected_into_system_prompt(tmp_path) -> None: assert "# Recent History" in prompt assert "User asked about weather in Tokyo" in prompt assert "Agent fetched forecast via web_search" in prompt + assert re.search(r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]", prompt) + + +def test_recent_history_capped_at_max(tmp_path) -> None: + """Only the most recent _MAX_RECENT_HISTORY entries are injected.""" + workspace = _make_workspace(tmp_path) + builder = ContextBuilder(workspace) + + for i in range(builder._MAX_RECENT_HISTORY + 20): + builder.memory.append_history(f"entry-{i}") + + prompt = builder.build_system_prompt() + assert "entry-0" not in prompt + assert "entry-19" not in prompt + assert f"entry-{builder._MAX_RECENT_HISTORY + 19}" in prompt def test_no_recent_history_when_dream_has_processed_all(tmp_path) -> None: @@ -112,6 +128,26 @@ def test_no_recent_history_when_dream_has_processed_all(tmp_path) -> None: assert "# Recent History" not in prompt +def test_partial_dream_processing_shows_only_remainder(tmp_path) -> None: + """When Dream has processed some entries, only the unprocessed ones appear.""" + workspace = _make_workspace(tmp_path) + builder = ContextBuilder(workspace) + + c1 = builder.memory.append_history("old conversation about Python") + c2 = builder.memory.append_history("old conversation about Rust") + builder.memory.append_history("recent question about Docker") + builder.memory.append_history("recent question about K8s") + + builder.memory.set_last_dream_cursor(c2) + + prompt = builder.build_system_prompt() + assert "# Recent History" in prompt + assert "old conversation about Python" not in prompt + assert "old conversation about Rust" not in prompt + assert "recent question about Docker" in prompt + assert "recent question about K8s" in prompt + + def test_subagent_result_does_not_create_consecutive_assistant_messages(tmp_path) -> None: workspace = _make_workspace(tmp_path) builder = ContextBuilder(workspace)