diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 6834e879..a66b6fe9 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -1183,7 +1183,6 @@ class AgentLoop: _hist_kwargs: dict[str, Any] = { "max_messages": self._max_messages, "max_tokens": self._replay_token_budget(), - "include_timestamps": True, "extend_to_user": is_subagent, } history = session.get_history(**_hist_kwargs) @@ -1462,7 +1461,6 @@ class AgentLoop: _hist_kwargs: dict[str, Any] = { "max_messages": self._max_messages, "max_tokens": self._replay_token_budget(), - "include_timestamps": True, "extend_to_user": False, } ctx.history = ctx.session.get_history(**_hist_kwargs) diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index d2d32a2b..94916e2f 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -712,17 +712,12 @@ class Consolidator: @staticmethod def _full_unconsolidated_history( session: Session, - *, - include_timestamps: bool = False, ) -> list[dict[str, Any]]: """Return the whole unconsolidated tail for consolidation decisions.""" unconsolidated_count = len(session.messages) - session.last_consolidated if unconsolidated_count <= 0: return [] - return session.get_history( - max_messages=unconsolidated_count, - include_timestamps=include_timestamps, - ) + return session.get_history(max_messages=unconsolidated_count) @staticmethod def _replay_overflow_boundary( @@ -797,7 +792,7 @@ class Consolidator: session: Session, ) -> tuple[int, str]: """Estimate prompt size from the full unconsolidated session tail.""" - history = self._full_unconsolidated_history(session, include_timestamps=True) + history = self._full_unconsolidated_history(session) channel, chat_id = (session.key.split(":", 1) if ":" in session.key else (None, None)) # Include archived summary in estimation so the budget accounts for it. meta = session.metadata.get("_last_summary") diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 331ccf95..5bf9968f 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -118,25 +118,6 @@ class Session: ): self.last_consolidated = 0 - @staticmethod - def _annotate_message_time(message: dict[str, Any], content: Any) -> Any: - """Expose persisted turn timestamps to the model for relative-date reasoning. - - Annotating *every* assistant turn trains the model (via in-context - demonstrations) to start its own replies with the same - ``[Message Time: ...]`` prefix, which leaks metadata back to the user. - We therefore only annotate user turns. User-side stamps are enough to - pin adjacent assistant replies for relative-time reasoning, including - proactive messages the user replies to later. - """ - timestamp = message.get("timestamp") - if not timestamp or not isinstance(content, str): - return content - role = message.get("role") - if role != "user": - return content - return f"[Message Time: {timestamp}]\n{content}" - def add_message(self, role: str, content: str, **kwargs: Any) -> None: """Add a message to the session.""" msg = { @@ -153,7 +134,6 @@ class Session: max_messages: int = 120, *, max_tokens: int = 0, - include_timestamps: bool = False, extend_to_user: bool = False, ) -> list[dict[str, Any]]: """Return unconsolidated messages for LLM input. @@ -243,8 +223,6 @@ class Session: if mcp_lines: breadcrumbs = "\n".join(mcp_lines) content = f"{content}\n{breadcrumbs}" if content else breadcrumbs - if include_timestamps: - content = self._annotate_message_time(message, content) if role == "assistant" and isinstance(content, str) and not content.strip(): if not any(key in message for key in ("tool_calls", "reasoning_content", "thinking_blocks")): continue diff --git a/tests/agent/test_auto_compact.py b/tests/agent/test_auto_compact.py index 398cbc95..fe63d7da 100644 --- a/tests/agent/test_auto_compact.py +++ b/tests/agent/test_auto_compact.py @@ -223,7 +223,7 @@ class TestAgentLoopTTLParam: kwargs = session.get_history.call_args.kwargs assert isinstance(kwargs.get("max_tokens"), int) assert kwargs["max_tokens"] > 0 - assert kwargs["include_timestamps"] is True + assert set(kwargs) == {"max_messages", "max_tokens", "extend_to_user"} @pytest.mark.asyncio async def test_session_file_cap_archives_and_trims_old_messages(self, tmp_path): diff --git a/tests/agent/test_loop_save_turn.py b/tests/agent/test_loop_save_turn.py index d64eb13c..a6984ad4 100644 --- a/tests/agent/test_loop_save_turn.py +++ b/tests/agent/test_loop_save_turn.py @@ -1222,11 +1222,9 @@ async def test_system_subagent_followup_is_persisted_before_prompt_assembly(tmp_ non_system = [m for m in seen["initial_messages"] if m.get("role") != "system"] assert "question" in non_system[0]["content"] assert "working" in non_system[1]["content"] - # User turns carry the timestamp prefix so the model can reason about - # relative time. Assistant turns do NOT, otherwise the model treats those - # past replies as in-context examples and starts its own outputs with - # ``[Message Time: ...]`` (which then leaks back to the user). - assert "[Message Time:" in non_system[0]["content"] + # Persisted timestamps stay in session records, but replay content is not + # rewritten with volatile ``[Message Time: ...]`` prefixes. + assert "[Message Time:" not in non_system[0]["content"] assert "[Message Time:" not in non_system[1]["content"] assert non_system[2]["content"].count("subagent result") == 1 assert "Current Time:" in non_system[2]["content"] diff --git a/tests/agent/test_session_manager_history.py b/tests/agent/test_session_manager_history.py index dc9c648a..ab619cab 100644 --- a/tests/agent/test_session_manager_history.py +++ b/tests/agent/test_session_manager_history.py @@ -266,13 +266,8 @@ def test_get_history_preserves_reasoning_content(): ] -def test_get_history_annotates_user_turns_but_not_assistant_turns(): - """Only user turns carry the timestamp prefix. - - Annotating assistant turns trains the model (via in-context examples) to - start its own replies with ``[Message Time: ...]``. User-side stamps are - enough to pin adjacent assistant replies for relative-time reasoning. - """ +def test_get_history_does_not_inject_persisted_timestamps_into_replay_content(): + """Persisted timestamps are session metadata, not prompt content.""" session = Session(key="test:timestamps") session.messages.append({ "role": "user", @@ -285,12 +280,14 @@ def test_get_history_annotates_user_turns_but_not_assistant_turns(): "timestamp": "2026-04-26T22:00:05", }) - history = session.get_history(max_messages=500, include_timestamps=True) + history = session.get_history(max_messages=500) + assert session.messages[0]["timestamp"] == "2026-04-26T22:00:00" + assert session.messages[1]["timestamp"] == "2026-04-26T22:00:05" assert history == [ { "role": "user", - "content": "[Message Time: 2026-04-26T22:00:00]\n10 点提醒是昨天发生的", + "content": "10 点提醒是昨天发生的", }, { "role": "assistant", @@ -299,8 +296,8 @@ def test_get_history_annotates_user_turns_but_not_assistant_turns(): ] -def test_get_history_does_not_annotate_proactive_assistant_deliveries_with_timestamps(): - """Assistant-side timestamp examples can leak back into future replies.""" +def test_get_history_keeps_proactive_delivery_timestamps_out_of_replay_content(): + """Timestamp metadata remains persisted without becoming prompt text.""" session = Session(key="test:proactive-timestamps") session.messages.append({ "role": "assistant", @@ -314,8 +311,10 @@ def test_get_history_does_not_annotate_proactive_assistant_deliveries_with_times "timestamp": "2026-04-26T18:00:00", }) - history = session.get_history(max_messages=500, include_timestamps=True) + history = session.get_history(max_messages=500) + assert session.messages[0]["timestamp"] == "2026-04-26T15:00:00" + assert session.messages[1]["timestamp"] == "2026-04-26T18:00:00" assert history == [ { "role": "assistant", @@ -323,18 +322,18 @@ def test_get_history_does_not_annotate_proactive_assistant_deliveries_with_times }, { "role": "user", - "content": "[Message Time: 2026-04-26T18:00:00]\n好", + "content": "好", }, ] -def test_get_history_does_not_annotate_tool_results_with_timestamps(): +def test_get_history_does_not_inject_tool_result_timestamps(): session = Session(key="test:tool-timestamps") session.messages.append({"role": "user", "content": "run tool"}) session.messages.extend(_tool_turn("ts", 0)) session.messages[-1]["timestamp"] = "2026-04-26T22:00:10" - history = session.get_history(max_messages=500, include_timestamps=True) + history = session.get_history(max_messages=500) tool_result = history[-1] assert tool_result["role"] == "tool" @@ -555,7 +554,7 @@ def test_get_history_sanitizes_existing_assistant_replay_artifacts(): } ) - history = session.get_history(max_messages=500, include_timestamps=True) + history = session.get_history(max_messages=500) assert history == [{"role": "assistant", "content": "来了 🎨"}]