fix(gateway): preview memory prefetch context in chat

This commit is contained in:
james 2026-04-24 18:24:57 -05:00
parent 13038dc747
commit 2a1e0fc205
8 changed files with 508 additions and 9 deletions

View file

@ -202,6 +202,20 @@ class TestMemoryManager:
assert p1.prefetch_queries == ["what do you know?"]
assert p2.prefetch_queries == ["what do you know?"]
def test_prefetch_all_details_returns_provider_names(self):
mgr = MemoryManager()
p1 = FakeMemoryProvider("builtin")
p1._prefetch_result = "Memory from builtin"
p2 = FakeMemoryProvider("hindsight")
p2._prefetch_result = "Memory from hindsight"
mgr.add_provider(p1)
mgr.add_provider(p2)
text, providers = mgr.prefetch_all_details("what do you know?")
assert text == "Memory from builtin\n\nMemory from hindsight"
assert providers == ["builtin", "hindsight"]
def test_prefetch_skips_empty(self):
mgr = MemoryManager()
p1 = FakeMemoryProvider("builtin")

View file

@ -54,6 +54,23 @@ class TestResolveDisplaySetting:
# Unknown platform, no config → global default "all"
assert resolve_display_setting(config, "unknown_platform", "tool_progress") == "all"
def test_memory_context_defaults_to_preview_for_all_platforms(self):
"""Memory context defaults to bounded preview everywhere for dogfood visibility."""
from gateway.display_config import resolve_display_setting
config = {}
for plat in (
"discord",
"telegram",
"tui",
"api_server",
"webhook",
"email",
"unknown_platform",
):
assert resolve_display_setting(config, plat, "memory_context") == "preview", plat
assert resolve_display_setting(config, plat, "memory_context_max_chars") == 1200, plat
def test_fallback_parameter_used_last(self):
"""Explicit fallback is used when nothing else matches."""
from gateway.display_config import resolve_display_setting
@ -170,6 +187,28 @@ class TestYAMLNormalisation:
config = {"display": {"platforms": {"slack": {"tool_progress": False}}}}
assert resolve_display_setting(config, "slack", "tool_progress") == "off"
def test_memory_context_modes_normalised(self):
"""Memory context mode values are normalised and validated."""
from gateway.display_config import resolve_display_setting
assert resolve_display_setting(
{"display": {"memory_context": False}}, "discord", "memory_context"
) == "off"
assert resolve_display_setting(
{"display": {"memory_context": True}}, "discord", "memory_context"
) == "preview"
assert resolve_display_setting(
{"display": {"memory_context": "SUMMARY"}}, "discord", "memory_context"
) == "summary"
assert resolve_display_setting(
{"display": {"memory_context": "nonsense"}}, "discord", "memory_context"
) == "preview"
assert resolve_display_setting(
{"display": {"memory_context_max_chars": "80"}},
"discord",
"memory_context_max_chars",
) == 80
# ---------------------------------------------------------------------------
# Built-in platform defaults (tier system)

View file

@ -497,6 +497,67 @@ class VerboseAgent:
}
class MemoryPrefetchAgent:
"""Agent that emits Hindsight-style memory prefetch progress."""
PREVIEW = (
'# Hindsight Memory (persistent cross-session context)\n\n'
'Use this to answer questions about the user and prior sessions. Do not call tools to look up information that is already present\n\n'
'here.\n\n'
'* [{"role": "user", "content": "User: [jim] [] Hindsight health is green\\n[ ] memory.prefetch emits exactly once", '
'"timestamp": "2026-04-24T20:30:36.770766+00:00"}, '
'{"role": "assistant", "content": "Assistant: Updated current checklist:\\n\\n```md\\n[x] Hindsight health is green\\n```", '
'"timestamp": "2026-04-24T20:31:00.000000+00:00"}]'
)
def __init__(self, **kwargs):
self.tool_progress_callback = kwargs.get("tool_progress_callback")
self.tools = []
def run_conversation(self, message, conversation_history=None, task_id=None):
self.tool_progress_callback(
"memory.prefetch",
None,
self.PREVIEW,
None,
providers=["hindsight"],
chars=14528,
)
time.sleep(0.35)
return {
"final_response": "done",
"messages": [],
"api_calls": 1,
}
class RawMemoryContextPrefetchAgent(MemoryPrefetchAgent):
"""Agent that emits the raw injected wrapper shape seen in Discord dogfood."""
PREVIEW = (
'<memory-context>\n'
'[System note: The following is recalled memory context, NOT new user input. '
'Treat as informational background data.]\n\n'
'# Hindsight Memory (persistent cross-session context)\n'
'Use this to answer questions about the user and prior sessions. Do not call tools to look up information that is already present here.\n\n'
'- [[{"role": "user", "content": "User: [jim] this is what im seeing", '
'"timestamp": "2026-04-24T21:25:35.131663+00:00"}]]\n'
'</memory-context>'
)
class PlainTranscriptMemoryPrefetchAgent(MemoryPrefetchAgent):
"""Agent that emits plain Hindsight transcript lines without JSON payloads."""
PREVIEW = (
'[System note: The following is recalled memory context, NOT new user input.]\n'
'# Hindsight Memory (persistent cross-session context)\n'
'Use this to answer questions about the user and prior sessions.\n'
'Assistant: Live dashboard is up. Tailnet link is ready.\n'
'Assistant: hi jim. Ready.'
)
async def _run_with_agent(
monkeypatch,
tmp_path,
@ -708,6 +769,110 @@ async def test_run_agent_previewed_final_marks_already_sent(monkeypatch, tmp_pat
assert [call["content"] for call in adapter.sent] == ["You're welcome."]
@pytest.mark.asyncio
async def test_discord_memory_prefetch_preview_uses_markdown_not_raw_json(monkeypatch, tmp_path):
adapter, result = await _run_with_agent(
monkeypatch,
tmp_path,
MemoryPrefetchAgent,
session_id="sess-memory-prefetch-discord",
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
platform=Platform.DISCORD,
chat_id="discord-1",
chat_type="dm",
thread_id=None,
)
assert result["final_response"] == "done"
all_content = "\n".join(call["content"] for call in adapter.sent)
all_content += "\n".join(call["content"] for call in adapter.edits)
assert "🧠 **Loaded memory context from hindsight** (14528 chars)" in all_content
assert "> 1. Hindsight health is green" in all_content
assert "> 2. Updated current checklist:" in all_content
assert "**User**:" not in all_content
assert "**Assistant**:" not in all_content
assert "[jim]" not in all_content
assert '"role"' not in all_content
assert '"content"' not in all_content
assert "Use this to answer questions" not in all_content
assert "```" not in all_content
@pytest.mark.asyncio
async def test_discord_memory_prefetch_preview_hides_raw_memory_context_wrapper(monkeypatch, tmp_path):
adapter, result = await _run_with_agent(
monkeypatch,
tmp_path,
RawMemoryContextPrefetchAgent,
session_id="sess-memory-prefetch-discord-wrapper",
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
platform=Platform.DISCORD,
chat_id="discord-1",
chat_type="dm",
thread_id=None,
)
assert result["final_response"] == "done"
all_content = "\n".join(call["content"] for call in adapter.sent)
all_content += "\n".join(call["content"] for call in adapter.edits)
assert "🧠 **Loaded memory context from hindsight** (14528 chars)" in all_content
assert "> 1. this is what im seeing" in all_content
assert "**User**:" not in all_content
assert "[jim]" not in all_content
assert "<memory-context>" not in all_content
assert "</memory-context>" not in all_content
assert "[System note:" not in all_content
assert "# Hindsight Memory" not in all_content
assert "Use this to answer questions" not in all_content
assert '"role"' not in all_content
assert '"content"' not in all_content
@pytest.mark.asyncio
async def test_discord_memory_prefetch_preview_formats_plain_hindsight_transcript(monkeypatch, tmp_path):
adapter, result = await _run_with_agent(
monkeypatch,
tmp_path,
PlainTranscriptMemoryPrefetchAgent,
session_id="sess-memory-prefetch-discord-plain",
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
platform=Platform.DISCORD,
chat_id="discord-1",
chat_type="dm",
thread_id=None,
)
assert result["final_response"] == "done"
all_content = "\n".join(call["content"] for call in adapter.sent)
all_content += "\n".join(call["content"] for call in adapter.edits)
assert "> 1. Live dashboard is up. Tailnet link is ready." in all_content
assert "> 2. hi jim. Ready." in all_content
assert "Assistant:" not in all_content
assert "# Hindsight Memory" not in all_content
assert "Use this to answer questions" not in all_content
@pytest.mark.asyncio
async def test_memory_prefetch_progress_is_discord_only(monkeypatch, tmp_path):
adapter, result = await _run_with_agent(
monkeypatch,
tmp_path,
MemoryPrefetchAgent,
session_id="sess-memory-prefetch-telegram",
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
platform=Platform.TELEGRAM,
chat_id="-1001",
chat_type="group",
thread_id="17585",
)
assert result["final_response"] == "done"
all_content = "\n".join(call["content"] for call in adapter.sent)
all_content += "\n".join(call["content"] for call in adapter.edits)
assert "Loaded memory context" not in all_content
assert "Hindsight health is green" not in all_content
@pytest.mark.asyncio
async def test_run_agent_matrix_streaming_omits_cursor(monkeypatch, tmp_path):
adapter, result = await _run_with_agent(

View file

@ -4771,6 +4771,110 @@ class TestMemoryContextSanitization:
assert "stale observation" not in result
assert "how is the honcho working" in result
def test_memory_prefetch_emits_progress_with_provider_names(self, agent):
raw_memory = (
"# Hindsight Memory (persistent cross-session context)\n"
"Use this to answer questions about the user and prior sessions.\n\n"
'- [{"role": "user", "content": "User: [jim] prior useful context"}]\n'
)
class FakeMemoryManager:
def on_turn_start(self, *_args, **_kwargs):
pass
def prefetch_all_details(self, query, *, session_id=""):
assert query == "hello"
return raw_memory, ["hindsight"]
def sync_all(self, *_args, **_kwargs):
pass
def queue_prefetch_all(self, *_args, **_kwargs):
pass
events = []
def capture_progress(*args, **kwargs):
events.append((args, kwargs))
agent._cached_system_prompt = "You are helpful."
agent._use_prompt_caching = False
agent.tool_delay = 0
agent.compression_enabled = False
agent.save_trajectories = False
agent._memory_manager = FakeMemoryManager()
agent.tool_progress_callback = capture_progress
agent.client.chat.completions.create.return_value = _mock_response(
content="Final answer",
finish_reason="stop",
)
with (
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
):
result = agent.run_conversation("hello")
assert result["final_response"] == "Final answer"
sent_messages = agent.client.chat.completions.create.call_args.kwargs["messages"]
assert "<memory-context>" in sent_messages[-1]["content"]
assert "prior useful context" in sent_messages[-1]["content"]
memory_events = [(args, kwargs) for args, kwargs in events if args[0] == "memory.prefetch"]
assert len(memory_events) == 1
args, kwargs = memory_events[0]
assert args[:2] == ("memory.prefetch", "memory")
assert "prior useful context" in args[2]
assert kwargs["providers"] == ["hindsight"]
assert kwargs["provider_count"] == 1
assert kwargs["chars"] == len(raw_memory)
assert kwargs["injected"] is True
def test_memory_prefetch_progress_only_emits_when_context_block_injected(self, agent):
raw_memory = "memory text"
class FakeMemoryManager:
def on_turn_start(self, *_args, **_kwargs):
pass
def prefetch_all_details(self, query, *, session_id=""):
assert query == "hello"
return raw_memory, ["hindsight"]
def sync_all(self, *_args, **_kwargs):
pass
def queue_prefetch_all(self, *_args, **_kwargs):
pass
events = []
def capture_progress(*args, **kwargs):
events.append((args, kwargs))
agent._cached_system_prompt = "You are helpful."
agent._use_prompt_caching = False
agent.tool_delay = 0
agent.compression_enabled = False
agent.save_trajectories = False
agent._memory_manager = FakeMemoryManager()
agent.tool_progress_callback = capture_progress
agent.client.chat.completions.create.return_value = _mock_response(
content="Final answer",
finish_reason="stop",
)
with (
patch.object(run_agent, "build_memory_context_block", return_value=""),
patch.object(agent, "_persist_session"),
patch.object(agent, "_save_trajectory"),
patch.object(agent, "_cleanup_task_resources"),
):
result = agent.run_conversation("hello")
assert result["final_response"] == "Final answer"
assert [args for args, _kwargs in events if args[0] == "memory.prefetch"] == []
class TestMemoryProviderTurnStart:
"""run_conversation() must call memory_manager.on_turn_start() before prefetch_all().