fix(session): remove message time replay prefixes
This commit is contained in:
@@ -1183,7 +1183,6 @@ class AgentLoop:
|
|||||||
_hist_kwargs: dict[str, Any] = {
|
_hist_kwargs: dict[str, Any] = {
|
||||||
"max_messages": self._max_messages,
|
"max_messages": self._max_messages,
|
||||||
"max_tokens": self._replay_token_budget(),
|
"max_tokens": self._replay_token_budget(),
|
||||||
"include_timestamps": True,
|
|
||||||
"extend_to_user": is_subagent,
|
"extend_to_user": is_subagent,
|
||||||
}
|
}
|
||||||
history = session.get_history(**_hist_kwargs)
|
history = session.get_history(**_hist_kwargs)
|
||||||
@@ -1462,7 +1461,6 @@ class AgentLoop:
|
|||||||
_hist_kwargs: dict[str, Any] = {
|
_hist_kwargs: dict[str, Any] = {
|
||||||
"max_messages": self._max_messages,
|
"max_messages": self._max_messages,
|
||||||
"max_tokens": self._replay_token_budget(),
|
"max_tokens": self._replay_token_budget(),
|
||||||
"include_timestamps": True,
|
|
||||||
"extend_to_user": False,
|
"extend_to_user": False,
|
||||||
}
|
}
|
||||||
ctx.history = ctx.session.get_history(**_hist_kwargs)
|
ctx.history = ctx.session.get_history(**_hist_kwargs)
|
||||||
|
|||||||
@@ -712,17 +712,12 @@ class Consolidator:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _full_unconsolidated_history(
|
def _full_unconsolidated_history(
|
||||||
session: Session,
|
session: Session,
|
||||||
*,
|
|
||||||
include_timestamps: bool = False,
|
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Return the whole unconsolidated tail for consolidation decisions."""
|
"""Return the whole unconsolidated tail for consolidation decisions."""
|
||||||
unconsolidated_count = len(session.messages) - session.last_consolidated
|
unconsolidated_count = len(session.messages) - session.last_consolidated
|
||||||
if unconsolidated_count <= 0:
|
if unconsolidated_count <= 0:
|
||||||
return []
|
return []
|
||||||
return session.get_history(
|
return session.get_history(max_messages=unconsolidated_count)
|
||||||
max_messages=unconsolidated_count,
|
|
||||||
include_timestamps=include_timestamps,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _replay_overflow_boundary(
|
def _replay_overflow_boundary(
|
||||||
@@ -797,7 +792,7 @@ class Consolidator:
|
|||||||
session: Session,
|
session: Session,
|
||||||
) -> tuple[int, str]:
|
) -> tuple[int, str]:
|
||||||
"""Estimate prompt size from the full unconsolidated session tail."""
|
"""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))
|
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.
|
# Include archived summary in estimation so the budget accounts for it.
|
||||||
meta = session.metadata.get("_last_summary")
|
meta = session.metadata.get("_last_summary")
|
||||||
|
|||||||
@@ -118,25 +118,6 @@ class Session:
|
|||||||
):
|
):
|
||||||
self.last_consolidated = 0
|
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:
|
def add_message(self, role: str, content: str, **kwargs: Any) -> None:
|
||||||
"""Add a message to the session."""
|
"""Add a message to the session."""
|
||||||
msg = {
|
msg = {
|
||||||
@@ -153,7 +134,6 @@ class Session:
|
|||||||
max_messages: int = 120,
|
max_messages: int = 120,
|
||||||
*,
|
*,
|
||||||
max_tokens: int = 0,
|
max_tokens: int = 0,
|
||||||
include_timestamps: bool = False,
|
|
||||||
extend_to_user: bool = False,
|
extend_to_user: bool = False,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Return unconsolidated messages for LLM input.
|
"""Return unconsolidated messages for LLM input.
|
||||||
@@ -243,8 +223,6 @@ class Session:
|
|||||||
if mcp_lines:
|
if mcp_lines:
|
||||||
breadcrumbs = "\n".join(mcp_lines)
|
breadcrumbs = "\n".join(mcp_lines)
|
||||||
content = f"{content}\n{breadcrumbs}" if content else breadcrumbs
|
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 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")):
|
if not any(key in message for key in ("tool_calls", "reasoning_content", "thinking_blocks")):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class TestAgentLoopTTLParam:
|
|||||||
kwargs = session.get_history.call_args.kwargs
|
kwargs = session.get_history.call_args.kwargs
|
||||||
assert isinstance(kwargs.get("max_tokens"), int)
|
assert isinstance(kwargs.get("max_tokens"), int)
|
||||||
assert kwargs["max_tokens"] > 0
|
assert kwargs["max_tokens"] > 0
|
||||||
assert kwargs["include_timestamps"] is True
|
assert set(kwargs) == {"max_messages", "max_tokens", "extend_to_user"}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_session_file_cap_archives_and_trims_old_messages(self, tmp_path):
|
async def test_session_file_cap_archives_and_trims_old_messages(self, tmp_path):
|
||||||
|
|||||||
@@ -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"]
|
non_system = [m for m in seen["initial_messages"] if m.get("role") != "system"]
|
||||||
assert "question" in non_system[0]["content"]
|
assert "question" in non_system[0]["content"]
|
||||||
assert "working" in non_system[1]["content"]
|
assert "working" in non_system[1]["content"]
|
||||||
# User turns carry the timestamp prefix so the model can reason about
|
# Persisted timestamps stay in session records, but replay content is not
|
||||||
# relative time. Assistant turns do NOT, otherwise the model treats those
|
# rewritten with volatile ``[Message Time: ...]`` prefixes.
|
||||||
# past replies as in-context examples and starts its own outputs with
|
assert "[Message Time:" not in non_system[0]["content"]
|
||||||
# ``[Message Time: ...]`` (which then leaks back to the user).
|
|
||||||
assert "[Message Time:" in non_system[0]["content"]
|
|
||||||
assert "[Message Time:" not in non_system[1]["content"]
|
assert "[Message Time:" not in non_system[1]["content"]
|
||||||
assert non_system[2]["content"].count("subagent result") == 1
|
assert non_system[2]["content"].count("subagent result") == 1
|
||||||
assert "Current Time:" in non_system[2]["content"]
|
assert "Current Time:" in non_system[2]["content"]
|
||||||
|
|||||||
@@ -266,13 +266,8 @@ def test_get_history_preserves_reasoning_content():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_get_history_annotates_user_turns_but_not_assistant_turns():
|
def test_get_history_does_not_inject_persisted_timestamps_into_replay_content():
|
||||||
"""Only user turns carry the timestamp prefix.
|
"""Persisted timestamps are session metadata, not prompt content."""
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
session = Session(key="test:timestamps")
|
session = Session(key="test:timestamps")
|
||||||
session.messages.append({
|
session.messages.append({
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -285,12 +280,14 @@ def test_get_history_annotates_user_turns_but_not_assistant_turns():
|
|||||||
"timestamp": "2026-04-26T22:00:05",
|
"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 == [
|
assert history == [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": "[Message Time: 2026-04-26T22:00:00]\n10 点提醒是昨天发生的",
|
"content": "10 点提醒是昨天发生的",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"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():
|
def test_get_history_keeps_proactive_delivery_timestamps_out_of_replay_content():
|
||||||
"""Assistant-side timestamp examples can leak back into future replies."""
|
"""Timestamp metadata remains persisted without becoming prompt text."""
|
||||||
session = Session(key="test:proactive-timestamps")
|
session = Session(key="test:proactive-timestamps")
|
||||||
session.messages.append({
|
session.messages.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
@@ -314,8 +311,10 @@ def test_get_history_does_not_annotate_proactive_assistant_deliveries_with_times
|
|||||||
"timestamp": "2026-04-26T18:00:00",
|
"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 == [
|
assert history == [
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
@@ -323,18 +322,18 @@ def test_get_history_does_not_annotate_proactive_assistant_deliveries_with_times
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"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 = Session(key="test:tool-timestamps")
|
||||||
session.messages.append({"role": "user", "content": "run tool"})
|
session.messages.append({"role": "user", "content": "run tool"})
|
||||||
session.messages.extend(_tool_turn("ts", 0))
|
session.messages.extend(_tool_turn("ts", 0))
|
||||||
session.messages[-1]["timestamp"] = "2026-04-26T22:00:10"
|
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]
|
tool_result = history[-1]
|
||||||
assert tool_result["role"] == "tool"
|
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": "来了 🎨"}]
|
assert history == [{"role": "assistant", "content": "来了 🎨"}]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user