diff --git a/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py b/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py index 9eba97057..e39a78633 100644 --- a/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py +++ b/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py @@ -59,11 +59,12 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] async with shielded_async_session() as session: if self.user_id is not None: - user_memory = await self._load_user_memory(session) + user_memory, is_persisted = await self._load_user_memory(session) if user_memory: chars = len(user_memory) + persisted = "true" if is_persisted else "false" memory_blocks.append( - f'\n' + f'\n' f"{user_memory}\n" f"" ) @@ -90,7 +91,16 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] return {"messages": new_messages} - async def _load_user_memory(self, session: AsyncSession) -> str | None: + async def _load_user_memory( + self, session: AsyncSession + ) -> tuple[str | None, bool]: + """Return (memory_content, is_persisted). + + When the user has saved memory in the database, ``is_persisted`` is + ``True``. When we fall back to a seed (first-name only), it is + ``False`` — the system prompt instructs the LLM to call + ``update_memory`` once to persist it. + """ try: result = await session.execute( select(User.memory_md, User.display_name).where( @@ -99,25 +109,21 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] ) row = result.one_or_none() if row is None: - return None + return None, True memory_md, display_name = row if memory_md: - return memory_md + return memory_md, True - # No saved memory yet — seed with the user's first name so the - # LLM knows who it's talking to from the very first turn. The - # name is only injected, not persisted; the LLM will include it - # naturally when it first calls update_memory. if display_name: first_name = display_name.split()[0] - return f"## About the user\n- Name: {first_name}" + return f"## About the user\n- Name: {first_name}", False - return None + return None, True except Exception: logger.exception("Failed to load user memory") - return None + return None, True async def _load_team_memory(self, session: AsyncSession) -> str | None: try: diff --git a/surfsense_backend/app/agents/new_chat/system_prompt.py b/surfsense_backend/app/agents/new_chat/system_prompt.py index 39a4704ae..9c9a45b52 100644 --- a/surfsense_backend/app/agents/new_chat/system_prompt.py +++ b/surfsense_backend/app/agents/new_chat/system_prompt.py @@ -257,12 +257,18 @@ _MEMORY_TOOL_INSTRUCTIONS: dict[str, dict[str, str]] = { `limit` attributes show your current usage and the maximum allowed size. - This is your curated long-term memory — the distilled essence of what you know about the user, not raw conversation logs. - - Call update_memory ONLY when the user shares genuinely important long-term information: + - **If has persisted="false"**, the memory has NOT been saved to the + database yet (it is a seed). You MUST call update_memory during this conversation to + persist it, merging in any new information you learn from the user. + - Call update_memory when the user shares long-term information about themselves, + whether explicitly or casually as part of a request: + * Identity & background mentioned in passing: "I'm a student", "at my company we...", + "I'm working on a React app", "I'm a data scientist" * Explicit requests: "remember this", "keep in mind", "note that" * Preferences: "I prefer X", "I like Y", "always do Z" * Facts: name, role, company, expertise, current projects * Standing instructions: response format, communication style - - Do NOT call for trivial, temporary, or conversation-specific information. + - Skip truly ephemeral info (one-off questions, greetings, session logistics). - Args: - updated_memory: The FULL updated markdown document (not a diff). Merge new facts with existing ones, update contradictions, remove outdated entries. @@ -281,12 +287,13 @@ _MEMORY_TOOL_INSTRUCTIONS: dict[str, dict[str, str]] = { - Your current team memory is already in in your context. The `chars` and `limit` attributes show current usage and the maximum allowed size. - This is the team's curated long-term memory — decisions, conventions, key facts. - - Call update_memory ONLY when the team shares genuinely important long-term information: - * Team decisions: "let's remember we decided to use X" + - Call update_memory when the team shares long-term information, whether explicitly + or as part of a broader discussion: + * Team decisions: "let's use X", "we decided to go with Y" * Conventions: coding standards, processes, naming patterns * Key facts: where things are, how things work, team structure * Priorities: active projects, deadlines, blockers - - Do NOT call for trivial, temporary, or conversation-specific information. + - Skip truly ephemeral info (one-off questions, greetings, session logistics). - Args: - updated_memory: The FULL updated markdown document (not a diff). Merge new facts with existing ones, update contradictions, remove outdated entries. @@ -305,12 +312,20 @@ _MEMORY_TOOL_INSTRUCTIONS: dict[str, dict[str, str]] = { _MEMORY_TOOL_EXAMPLES: dict[str, dict[str, str]] = { "update_memory": { "private": """ +- contains "## About the user\\n- Name: Alex" + User: "I'm a university student, explain astrophage to me" + - Memory is not yet persisted AND the user casually shared that they are a student. + You MUST call update_memory to persist the seed plus the new fact: + update_memory(updated_memory="## About the user\\n- Name: Alex\\n- University student\\n") - User: "Remember that I prefer TypeScript over JavaScript" - Timeless preference, no date needed. You see the current and merge: update_memory(updated_memory="## About the user\\n- Senior developer\\n\\n## Preferences\\n- Prefers TypeScript over JavaScript\\n...") - User: "I actually moved to Google last month" - Fact that changes over time, include date: update_memory(updated_memory="## About the user\\n- Senior developer at Google (since 2026-03, previously Acme Corp)\\n...") +- User: "I'm building a SaaS app with Next.js and Supabase" + - Implicit project info shared as context. Save it: + update_memory(updated_memory="## About the user\\n- Name: Alex\\n\\n## Current context\\n- Building a SaaS app with Next.js and Supabase (2026-04)\\n") """, "shared": """ - User: "Let's remember that we decided to use GraphQL"