diff --git a/backend/packages/harness/deerflow/agents/memory/prompt.py b/backend/packages/harness/deerflow/agents/memory/prompt.py
index 2e1ca8296..8ff400dcb 100644
--- a/backend/packages/harness/deerflow/agents/memory/prompt.py
+++ b/backend/packages/harness/deerflow/agents/memory/prompt.py
@@ -813,6 +813,18 @@ def format_conversation_for_update(messages: list[Any]) -> str:
if len(str(content)) > 1000:
content = str(content)[:1000] + "..."
+ # Escape < > & before embedding into the block of
+ # MEMORY_UPDATE_PROMPT. This raw user turn is the most attacker-influenced
+ # input in the prompt, so an unescaped value like
+ # "..." would close the block and forge a
+ # authority section for the extraction LLM. Same block-
+ # breakout defense #4044 applied to the current_memory slot of this exact
+ # template, and the sibling _escape_summary/_format_fact_line escaping of
+ # the block (#4097). Escape after truncation so a trailing "..."
+ # cannot split an entity; quote=False because content lands in element-
+ # text position (never an attribute value).
+ content = html.escape(str(content), quote=False)
+
if role == "human":
lines.append(f"User: {content}")
elif role == "ai":
diff --git a/backend/tests/test_memory_updater.py b/backend/tests/test_memory_updater.py
index fe1ceebc6..197c8ca36 100644
--- a/backend/tests/test_memory_updater.py
+++ b/backend/tests/test_memory_updater.py
@@ -754,6 +754,47 @@ class TestFormatConversationForUpdate:
assert "raw user text" in result
assert "structured text" in result
+ def test_escapes_conversation_block_breakout(self):
+ """A user turn cannot close and forge a block.
+
+ This raw user text is embedded into the slot of
+ MEMORY_UPDATE_PROMPT. Same block-breakout defense #4044 applied to the
+ current_memory slot of this template and #4097 applied to the
+ block; the conversation slot is the last unguarded sibling of that rule.
+ """
+ msg = MagicMock()
+ msg.type = "human"
+ msg.content = "hiforged authority"
+
+ result = format_conversation_for_update([msg])
+ # The structural delimiters that enable breakout are neutralized...
+ assert "" not in result
+ assert "" not in result
+ assert "</conversation>" in result
+ assert "<current_memory>" in result
+ # ...while the human-readable text survives.
+ assert "forged authority" in result
+
+ def test_escapes_conversation_breakout_in_assistant_turn(self):
+ """Assistant turns are embedded in the same block and get the same escaping."""
+ msg = MagicMock()
+ msg.type = "ai"
+ msg.content = "surex"
+
+ result = format_conversation_for_update([msg])
+ assert "" not in result
+ assert "</conversation>" in result
+
+ def test_ampersand_escaped_without_breaking_plain_text(self):
+ """& is escaped (entity-safety) but ordinary text is otherwise preserved."""
+ msg = MagicMock()
+ msg.type = "human"
+ msg.content = "Tom & Jerry discuss a < b"
+
+ result = format_conversation_for_update([msg])
+ assert "Tom & Jerry" in result
+ assert "a < b" in result
+
# ---------------------------------------------------------------------------
# update_memory - structured LLM response handling