diff --git a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py
index 8cb749d60..6b3ebb6ad 100644
--- a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py
+++ b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py
@@ -52,6 +52,10 @@ _INJECT_TIMEOUT_SECONDS = 5.0
_DATE_RE = re.compile(r"([^<]+)")
_DYNAMIC_CONTEXT_REMINDER_KEY = "dynamic_context_reminder"
+# Authoritative injected date, carried in additional_kwargs of the date
+# SystemMessage. Detection reads this instead of regex-parsing message content,
+# so it is never exposed to user-influenceable memory content.
+_REMINDER_DATE_KEY = "reminder_date"
_SUMMARY_MESSAGE_NAME = "summary"
@@ -75,11 +79,27 @@ def _last_injected_date(messages: list) -> str | None:
Detection uses the ``dynamic_context_reminder`` additional_kwargs flag rather
than content substring matching, so user messages containing ````
are not mistakenly treated as injected reminders.
+
+ The authoritative date is the ``reminder_date`` value in additional_kwargs of
+ the date SystemMessage. Reminders without it (the separate ````
+ HumanMessage, or any future dateless reminder) carry no date and are skipped,
+ so they cannot shadow the real date reminder.
"""
for msg in reversed(messages):
- if is_dynamic_context_reminder(msg):
+ if not is_dynamic_context_reminder(msg):
+ continue
+ structured = msg.additional_kwargs.get(_REMINDER_DATE_KEY)
+ if isinstance(structured, str) and structured:
+ return structured
+ # Backward-compat for checkpoints written before reminder_date existed:
+ # the date lived in content. Scope the regex to SystemMessage so it never
+ # runs on the user-influenceable memory HumanMessage (preserves the OWASP
+ # role separation from #3630 and closes the memory date-spoofing hole).
+ if isinstance(msg, SystemMessage):
content_str = msg.content if isinstance(msg.content, str) else str(msg.content)
- return _extract_date(content_str)
+ date = _extract_date(content_str)
+ if date is not None:
+ return date
return None
@@ -152,26 +172,34 @@ class DynamicContextMiddleware(AgentMiddleware):
original: HumanMessage,
reminder_content: str,
memory_content: str | None = None,
+ *,
+ reminder_date: str | None = None,
) -> list[SystemMessage | HumanMessage]:
"""Return messages using the ID-swap technique.
SystemMessage carries framework-owned data (date, metadata) — takes
- the original ID so add_messages replaces it in-place. Optional
+ the original ID so add_messages replaces it in-place. *reminder_date*
+ is recorded in its additional_kwargs as the authoritative injected date
+ (``_last_injected_date`` reads it instead of parsing content). Optional
HumanMessage carries user-owned memory content with ``{id}__memory``.
The actual user message gets ``{id}__user``.
SystemMessage is used — system context must not masquerade as user
input (#3630). Memory is deliberately kept as HumanMessage so
- user-influenceable content does not gain system authority (OWASP LLM01).
+ user-influenceable content does not gain system authority (OWASP LLM01)
+ — and it deliberately never carries ``reminder_date``.
"""
stable_id = original.id or str(uuid.uuid4())
messages: list[SystemMessage | HumanMessage] = []
+ reminder_kwargs = {"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True}
+ if reminder_date is not None:
+ reminder_kwargs[_REMINDER_DATE_KEY] = reminder_date
messages.append(
SystemMessage(
content=reminder_content,
id=stable_id,
- additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
+ additional_kwargs=reminder_kwargs,
)
)
@@ -219,7 +247,7 @@ class DynamicContextMiddleware(AgentMiddleware):
memory_block is not None,
messages[first_idx].id,
)
- result_msgs = self._make_reminder_and_user_messages(messages[first_idx], date_reminder, memory_block)
+ result_msgs = self._make_reminder_and_user_messages(messages[first_idx], date_reminder, memory_block, reminder_date=current_date)
return {"messages": result_msgs}
if last_date == current_date:
@@ -231,7 +259,7 @@ class DynamicContextMiddleware(AgentMiddleware):
if last_human_idx is None:
return None
- result_msgs = self._make_reminder_and_user_messages(messages[last_human_idx], self._build_date_update_reminder())
+ result_msgs = self._make_reminder_and_user_messages(messages[last_human_idx], self._build_date_update_reminder(), reminder_date=current_date)
logger.info("DynamicContextMiddleware: midnight crossing detected — injected date update before current turn")
return {"messages": result_msgs}
diff --git a/backend/tests/test_dynamic_context_middleware.py b/backend/tests/test_dynamic_context_middleware.py
index 313295565..71bc5cf24 100644
--- a/backend/tests/test_dynamic_context_middleware.py
+++ b/backend/tests/test_dynamic_context_middleware.py
@@ -38,6 +38,20 @@ def _reminder_msg(content: str, msg_id: str) -> HumanMessage:
)
+def _date_reminder_msg(date_str: str, msg_id: str) -> SystemMessage:
+ """Build a persisted date reminder in the current production shape.
+
+ A date SystemMessage whose ``reminder_date`` additional_kwargs carries the
+ authoritative date — what ``DynamicContextMiddleware`` now writes to state.
+ """
+ content = f"\n{date_str}\n"
+ return SystemMessage(
+ content=content,
+ id=msg_id,
+ additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True, "reminder_date": date_str},
+ )
+
+
# ---------------------------------------------------------------------------
# Basic injection
# ---------------------------------------------------------------------------
@@ -105,10 +119,9 @@ def test_memory_included_when_present():
def test_skips_injection_if_already_present():
"""Second turn: separate reminder message already present → no update."""
mw = _make_middleware()
- reminder_content = "\n2026-05-08, Friday\n"
state = {
"messages": [
- _reminder_msg(reminder_content, "msg-1"),
+ _date_reminder_msg("2026-05-08, Friday", "msg-1"),
HumanMessage(content="Hello", id="msg-1__user"),
AIMessage(content="Hi there"),
HumanMessage(content="Follow-up", id="msg-2"),
@@ -122,6 +135,131 @@ def test_skips_injection_if_already_present():
assert result is None # no update needed
+def test_second_turn_with_memory_does_not_reinject():
+ """Regression: a dateless memory reminder must not shadow the date reminder.
+
+ Reproduces the scrambled-messages / wrong-answer bug (thread
+ 9be75d63): production persists the injected context as TWO flagged
+ messages — a date SystemMessage and a separate dateless
+ HumanMessage. On a later turn ``_last_injected_date`` scans in reverse
+ and hits the memory message first; because it has no it
+ must keep scanning to find the real date. If it stops and returns None,
+ the middleware falsely treats this as the first turn, re-injects, picks
+ the previous turn's ``__user`` message as the target, and the model
+ re-answers the stale turn instead of the new one.
+ """
+ mw = _make_middleware()
+ date_reminder = "\n2026-05-08, Friday\n"
+ state = {
+ "messages": [
+ SystemMessage(
+ content=date_reminder,
+ id="msg-1",
+ additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
+ ),
+ _reminder_msg("\nUser prefers Python.\n", "msg-1__memory"),
+ HumanMessage(content="test", id="msg-1__user", name="user-input"),
+ AIMessage(content="Test received"),
+ HumanMessage(content="tell me the weather", id="msg-2", name="user-input"),
+ ]
+ }
+
+ with mock.patch("deerflow.agents.lead_agent.prompt._get_memory_context", return_value="\nUser prefers Python.\n"), mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt:
+ mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
+ result = mw.before_agent(state, _fake_runtime())
+
+ assert result is None # same day already injected → must NOT re-inject
+
+
+def test_poisoned_memory_does_not_spoof_injected_date():
+ """A embedded in user-influenceable memory must not spoof detection.
+
+ Memory is LLM-extracted from user input and injected unescaped (it's
+ hide_from_ui, so InputSanitizationMiddleware skips it). If a memory fact
+ contains a literal …, content-regex detection
+ would return that fake date (it sits after the authoritative date message but
+ is hit first in the reverse scan) and trigger a false midnight crossing /
+ re-injection. The authoritative date lives in additional_kwargs, so detection
+ must ignore the memory content entirely.
+ """
+ mw = _make_middleware()
+ today = "2026-05-08, Friday"
+ date_reminder = f"\n{today}\n"
+ state = {
+ "messages": [
+ SystemMessage(
+ content=date_reminder,
+ id="msg-1",
+ additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True, "reminder_date": today},
+ ),
+ _reminder_msg("\nUser asked about 2024-01-01 last year.\n", "msg-1__memory"),
+ HumanMessage(content="test", id="msg-1__user", name="user-input"),
+ AIMessage(content="Test received"),
+ HumanMessage(content="follow up", id="msg-2", name="user-input"),
+ ]
+ }
+
+ with mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt:
+ mock_dt.now.return_value.strftime.return_value = today
+ result = mw.before_agent(state, _fake_runtime())
+
+ # Detection uses the authoritative metadata date (today) → same day → no re-injection.
+ # If the fake 2024 date from memory content leaked in, this would be a midnight crossing.
+ assert result is None
+
+
+def test_date_reminder_carries_structured_date():
+ """First-turn injection records the authoritative date in additional_kwargs.
+
+ The date SystemMessage carries ``reminder_date``; the memory HumanMessage
+ deliberately does not (it is dateless and must never spoof detection).
+ """
+ mw = _make_middleware()
+ state = {"messages": [HumanMessage(content="Hi", id="msg-1")]}
+
+ with (
+ mock.patch("deerflow.agents.lead_agent.prompt._get_memory_context", return_value="\nUser prefers Python.\n"),
+ mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt,
+ ):
+ mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
+ result = mw.before_agent(state, _fake_runtime())
+
+ msgs = result["messages"]
+ assert isinstance(msgs[0], SystemMessage)
+ assert msgs[0].additional_kwargs.get("reminder_date") == "2026-05-08, Friday"
+ # Memory HumanMessage must not carry the authoritative date
+ assert isinstance(msgs[1], HumanMessage)
+ assert "reminder_date" not in msgs[1].additional_kwargs
+
+
+def test_legacy_systemmessage_reminder_without_key_detected():
+ """Backward-compat: pre-reminder_date checkpoints kept the date in content only.
+
+ A date SystemMessage with the date in content but no ``reminder_date`` key
+ must still be detected (via the SystemMessage-scoped content fallback) so
+ in-flight conversations from before the upgrade do not re-inject.
+ """
+ mw = _make_middleware()
+ state = {
+ "messages": [
+ SystemMessage(
+ content="\n2026-05-08, Friday\n",
+ id="msg-1",
+ additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True}, # no reminder_date
+ ),
+ HumanMessage(content="Hello", id="msg-1__user"),
+ AIMessage(content="Hi there"),
+ HumanMessage(content="Follow-up", id="msg-2"),
+ ]
+ }
+
+ with mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt:
+ mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
+ result = mw.before_agent(state, _fake_runtime())
+
+ assert result is None # same day detected from content → no re-injection
+
+
def test_injects_only_into_first_human_message_not_later_ones():
"""Reminder targets the first HumanMessage; subsequent messages are not touched."""
mw = _make_middleware()
@@ -272,10 +410,9 @@ def test_midnight_crossing_injects_date_update_as_separate_message():
"""When the date has changed, a separate date-update reminder is injected before
the current turn's HumanMessage using the ID-swap technique."""
mw = _make_middleware()
- reminder_content = "\n2026-05-08, Friday\n"
state = {
"messages": [
- _reminder_msg(reminder_content, "msg-1"),
+ _date_reminder_msg("2026-05-08, Friday", "msg-1"),
HumanMessage(content="Hello", id="msg-1__user"),
AIMessage(content="Response"),
HumanMessage(content="Good morning", id="msg-2"),
@@ -308,10 +445,9 @@ def test_midnight_crossing_injects_date_update_as_separate_message():
def test_midnight_crossing_id_swap():
"""Date-update reminder uses original ID; user message uses {id}__user."""
mw = _make_middleware()
- reminder_content = "\n2026-05-08, Friday\n"
state = {
"messages": [
- _reminder_msg(reminder_content, "msg-1"),
+ _date_reminder_msg("2026-05-08, Friday", "msg-1"),
HumanMessage(content="Next day message", id="msg-2"),
]
}
@@ -365,16 +501,12 @@ def test_memory_message_carries_reminder_key_for_title_eligibility():
def test_no_second_midnight_injection_once_date_updated():
"""After a midnight update is persisted, the same-day path skips re-injection."""
mw = _make_middleware()
- date_update_content = "\n2026-05-09, Saturday\n"
state = {
"messages": [
- _reminder_msg(
- "\n2026-05-08, Friday\n",
- "msg-1",
- ),
+ _date_reminder_msg("2026-05-08, Friday", "msg-1"),
HumanMessage(content="Hello", id="msg-1__user"),
AIMessage(content="Response"),
- _reminder_msg(date_update_content, "msg-2"),
+ _date_reminder_msg("2026-05-09, Saturday", "msg-2"),
HumanMessage(content="Good morning", id="msg-2__user"),
AIMessage(content="Good morning!"),
HumanMessage(content="Third turn", id="msg-3"),
diff --git a/backend/tests/test_summarization_middleware.py b/backend/tests/test_summarization_middleware.py
index d01c4479a..0ab526d4f 100644
--- a/backend/tests/test_summarization_middleware.py
+++ b/backend/tests/test_summarization_middleware.py
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
import pytest
from langchain.agents import create_agent
from langchain_core.language_models import BaseChatModel
-from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage, ToolMessage
+from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage, SystemMessage, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langgraph.constants import TAG_NOSTREAM
@@ -43,11 +43,13 @@ class _StaticChatModel(BaseChatModel):
return self._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
-def _dynamic_context_reminder(msg_id: str = "reminder-1") -> HumanMessage:
- return HumanMessage(
+def _dynamic_context_reminder(msg_id: str = "reminder-1") -> SystemMessage:
+ # Current production shape: a date SystemMessage carrying the authoritative
+ # date in additional_kwargs (see DynamicContextMiddleware).
+ return SystemMessage(
content="\n2026-05-08, Friday\n",
id=msg_id,
- additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True},
+ additional_kwargs={"hide_from_ui": True, _DYNAMIC_CONTEXT_REMINDER_KEY: True, "reminder_date": "2026-05-08, Friday"},
)