From ba7e288879de1fb791e35a3e56b46282e830b789 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:21:24 +0530 Subject: [PATCH 01/41] feat: add memory_md columns to user and searchspaces tables --- .../versions/121_add_memory_md_columns.py | 38 +++++++++++++++++++ surfsense_backend/app/db.py | 6 +++ 2 files changed, 44 insertions(+) create mode 100644 surfsense_backend/alembic/versions/121_add_memory_md_columns.py diff --git a/surfsense_backend/alembic/versions/121_add_memory_md_columns.py b/surfsense_backend/alembic/versions/121_add_memory_md_columns.py new file mode 100644 index 000000000..d5ff967fd --- /dev/null +++ b/surfsense_backend/alembic/versions/121_add_memory_md_columns.py @@ -0,0 +1,38 @@ +"""Add memory_md columns to user and searchspaces tables + +Revision ID: 121 +Revises: 120 + +Changes: +1. Add memory_md TEXT column to user table (personal memory) +2. Add shared_memory_md TEXT column to searchspaces table (team memory) +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import sqlalchemy as sa + +from alembic import op + +revision: str = "121" +down_revision: str | None = "120" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "user", + sa.Column("memory_md", sa.Text(), nullable=True, server_default=""), + ) + op.add_column( + "searchspaces", + sa.Column("shared_memory_md", sa.Text(), nullable=True, server_default=""), + ) + + +def downgrade() -> None: + op.drop_column("searchspaces", "shared_memory_md") + op.drop_column("user", "memory_md") diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 77a001a0d..98c1502b3 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -1392,6 +1392,8 @@ class SearchSpace(BaseModel, TimestampMixin): Text, nullable=True, default="" ) # User's custom instructions + shared_memory_md = Column(Text, nullable=True, server_default="") + # Search space-level LLM preferences (shared by all members) # Note: ID values: # - 0: Auto mode (uses LiteLLM Router for load balancing) - default for new search spaces @@ -2063,6 +2065,8 @@ if config.AUTH_TYPE == "GOOGLE": last_login = Column(TIMESTAMP(timezone=True), nullable=True) + memory_md = Column(Text, nullable=True, server_default="") + # Refresh tokens for this user refresh_tokens = relationship( "RefreshToken", @@ -2183,6 +2187,8 @@ else: last_login = Column(TIMESTAMP(timezone=True), nullable=True) + memory_md = Column(Text, nullable=True, server_default="") + # Refresh tokens for this user refresh_tokens = relationship( "RefreshToken", From 702d955e7945916dbd6a557672bc13fbd44c95c5 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:23:06 +0530 Subject: [PATCH 02/41] feat: integrate MemoryInjectionMiddleware and update memory tool instructions --- .../app/agents/new_chat/chat_deepagent.py | 9 ++ .../app/agents/new_chat/system_prompt.py | 151 +++++++----------- 2 files changed, 65 insertions(+), 95 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py index fc1e80d28..d788adcc3 100644 --- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py +++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py @@ -38,6 +38,7 @@ from app.agents.new_chat.llm_config import AgentConfig from app.agents.new_chat.middleware import ( DedupHITLToolCallsMiddleware, KnowledgeBaseSearchMiddleware, + MemoryInjectionMiddleware, SurfSenseFilesystemMiddleware, ) from app.agents.new_chat.system_prompt import ( @@ -425,9 +426,16 @@ async def create_surfsense_deep_agent( ) # -- Build the middleware stack (mirrors create_deep_agent internals) ------ + _memory_middleware = MemoryInjectionMiddleware( + user_id=user_id, + search_space_id=search_space_id, + thread_visibility=visibility, + ) + # General-purpose subagent middleware gp_middleware = [ TodoListMiddleware(), + _memory_middleware, SurfSenseFilesystemMiddleware( search_space_id=search_space_id, created_by_id=user_id, @@ -447,6 +455,7 @@ async def create_surfsense_deep_agent( # Main agent middleware deepagent_middleware = [ TodoListMiddleware(), + _memory_middleware, KnowledgeBaseSearchMiddleware( llm=llm, search_space_id=search_space_id, diff --git a/surfsense_backend/app/agents/new_chat/system_prompt.py b/surfsense_backend/app/agents/new_chat/system_prompt.py index 3d2442be8..39a4704ae 100644 --- a/surfsense_backend/app/agents/new_chat/system_prompt.py +++ b/surfsense_backend/app/agents/new_chat/system_prompt.py @@ -250,113 +250,75 @@ _TOOL_INSTRUCTIONS["web_search"] = """ # Memory tool instructions have private and shared variants. # We store them keyed as "save_memory" / "recall_memory" with sub-keys. _MEMORY_TOOL_INSTRUCTIONS: dict[str, dict[str, str]] = { - "save_memory": { + "update_memory": { "private": """ -- save_memory: Save facts, preferences, or context for personalized responses. - - Use this when the user explicitly or implicitly shares information worth remembering. - - Trigger scenarios: - * User says "remember this", "keep this in mind", "note that", or similar - * User shares personal preferences (e.g., "I prefer Python over JavaScript") - * User shares facts about themselves (e.g., "I'm a senior developer at Company X") - * User gives standing instructions (e.g., "always respond in bullet points") - * User shares project context (e.g., "I'm working on migrating our codebase to TypeScript") +- update_memory: Update your personal memory document about the user. + - Your current memory is already in in your context. The `chars` and + `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: + * 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. - Args: - - content: The fact/preference to remember. Phrase it clearly: - * "User prefers dark mode for all interfaces" - * "User is a senior Python developer" - * "User wants responses in bullet point format" - * "User is working on project called ProjectX" - - category: Type of memory: - * "preference": User preferences (coding style, tools, formats) - * "fact": Facts about the user (role, expertise, background) - * "instruction": Standing instructions (response format, communication style) - * "context": Current context (ongoing projects, goals, challenges) - - Returns: Confirmation of saved memory - - IMPORTANT: Only save information that would be genuinely useful for future conversations. - Don't save trivial or temporary information. + - updated_memory: The FULL updated markdown document (not a diff). + Merge new facts with existing ones, update contradictions, remove outdated entries. + Treat every update as a curation pass — consolidate, don't just append. + Include inline dates (YYYY-MM) on entries where temporal context matters (facts that + may change, decisions, context). Skip dates on timeless preferences and instructions. + - Keep it concise and well under the character limit shown in . + - Organize using markdown sections as appropriate (suggested but not required): + ## About the user — name, role, background, company (with date if it may change) + ## Preferences — languages, tools, frameworks, response style + ## Instructions — standing instructions, things to always/never do + ## Current context — ongoing projects, goals, deadlines (with date) """, "shared": """ -- save_memory: Save a fact, preference, or context to the team's shared memory for future reference. - - Use this when the user or a team member says "remember this", "keep this in mind", or similar in this shared chat. - - Use when the team agrees on something to remember (e.g., decisions, conventions). - - Someone shares a preference or fact that should be visible to the whole team. - - The saved information will be available in future shared conversations in this space. +- update_memory: Update the team's shared memory document for this search space. + - 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" + * 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. - Args: - - content: The fact/preference/context to remember. Phrase it clearly, e.g. "API keys are stored in Vault", "The team prefers weekly demos on Fridays" - - category: Type of memory. One of: - * "preference": Team or workspace preferences - * "fact": Facts the team agreed on (e.g., processes, locations) - * "instruction": Standing instructions for the team - * "context": Current context (e.g., ongoing projects, goals) - - Returns: Confirmation of saved memory; returned context may include who added it (added_by). - - IMPORTANT: Only save information that would be genuinely useful for future team conversations in this space. -""", - }, - "recall_memory": { - "private": """ -- recall_memory: Retrieve relevant memories about the user for personalized responses. - - Use this to access stored information about the user. - - Trigger scenarios: - * You need user context to give a better, more personalized answer - * User references something they mentioned before - * User asks "what do you know about me?" or similar - * Personalization would significantly improve response quality - * Before making recommendations that should consider user preferences - - Args: - - query: Optional search query to find specific memories (e.g., "programming preferences") - - category: Optional filter by category ("preference", "fact", "instruction", "context") - - top_k: Number of memories to retrieve (default: 5) - - Returns: Relevant memories formatted as context - - IMPORTANT: Use the recalled memories naturally in your response without explicitly - stating "Based on your memory..." - integrate the context seamlessly. -""", - "shared": """ -- recall_memory: Recall relevant team memories for this space to provide contextual responses. - - Use when you need team context to answer (e.g., "where do we store X?", "what did we decide about Y?"). - - Use when someone asks about something the team agreed to remember. - - Use when team preferences or conventions would improve the response. - - Args: - - query: Optional search query to find specific memories. If not provided, returns the most recent memories. - - category: Optional filter by category ("preference", "fact", "instruction", "context") - - top_k: Number of memories to retrieve (default: 5, max: 20) - - Returns: Relevant team memories and formatted context (may include added_by). Integrate naturally without saying "Based on team memory...". + - updated_memory: The FULL updated markdown document (not a diff). + Merge new facts with existing ones, update contradictions, remove outdated entries. + Treat every update as a curation pass — consolidate, don't just append. + Include inline dates (YYYY-MM) on decisions and time-sensitive entries. + - Keep it concise and well under the character limit shown in . + - Organize using markdown sections as appropriate (suggested but not required): + ## Team decisions — agreed-upon choices with rationale and date + ## Conventions — coding standards, tools, processes, naming patterns + ## Key facts — where things are, how things work, team structure + ## Current priorities — active projects, deadlines, blockers """, }, } _MEMORY_TOOL_EXAMPLES: dict[str, dict[str, str]] = { - "save_memory": { + "update_memory": { "private": """ - User: "Remember that I prefer TypeScript over JavaScript" - - Call: `save_memory(content="User prefers TypeScript over JavaScript for development", category="preference")` -- User: "I'm a data scientist working on ML pipelines" - - Call: `save_memory(content="User is a data scientist working on ML pipelines", category="fact")` -- User: "Always give me code examples in Python" - - Call: `save_memory(content="User wants code examples to be written in Python", category="instruction")` + - 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...") """, "shared": """ -- User: "Remember that API keys are stored in Vault" - - Call: `save_memory(content="API keys are stored in Vault", category="fact")` -- User: "Let's remember that the team prefers weekly demos on Fridays" - - Call: `save_memory(content="The team prefers weekly demos on Fridays", category="preference")` -""", - }, - "recall_memory": { - "private": """ -- User: "What programming language should I use for this project?" - - First recall: `recall_memory(query="programming language preferences")` - - Then provide a personalized recommendation based on their preferences -- User: "What do you know about me?" - - Call: `recall_memory(top_k=10)` - - Then summarize the stored memories -""", - "shared": """ -- User: "What did we decide about the release date?" - - First recall: `recall_memory(query="release date decision")` - - Then answer based on the team memories -- User: "Where do we document onboarding?" - - Call: `recall_memory(query="onboarding documentation")` - - Then answer using the recalled team context +- User: "Let's remember that we decided to use GraphQL" + - Decision with date: + update_memory(updated_memory="## Team decisions\\n- 2026-04: Adopted GraphQL over REST for new APIs\\n...") +- User: "Our deploy process uses Railway auto-deploys" + - Key fact, no date needed: + update_memory(updated_memory="## Key facts\\n- Deploy pipeline: git push -> Railway auto-deploys in ~3min\\n...") """, }, } @@ -456,8 +418,7 @@ _ALL_TOOL_NAMES_ORDERED = [ "generate_report", "generate_image", "scrape_webpage", - "save_memory", - "recall_memory", + "update_memory", ] From 5ff99115c69a12175648bda4c7bd2fd39909d456 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:23:25 +0530 Subject: [PATCH 03/41] feat: add MemoryInjectionMiddleware to inject user and team memory into conversation context --- .../agents/new_chat/middleware/__init__.py | 4 + .../new_chat/middleware/memory_injection.py | 115 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 surfsense_backend/app/agents/new_chat/middleware/memory_injection.py diff --git a/surfsense_backend/app/agents/new_chat/middleware/__init__.py b/surfsense_backend/app/agents/new_chat/middleware/__init__.py index 91996f702..1f6b12852 100644 --- a/surfsense_backend/app/agents/new_chat/middleware/__init__.py +++ b/surfsense_backend/app/agents/new_chat/middleware/__init__.py @@ -9,9 +9,13 @@ from app.agents.new_chat.middleware.filesystem import ( from app.agents.new_chat.middleware.knowledge_search import ( KnowledgeBaseSearchMiddleware, ) +from app.agents.new_chat.middleware.memory_injection import ( + MemoryInjectionMiddleware, +) __all__ = [ "DedupHITLToolCallsMiddleware", "KnowledgeBaseSearchMiddleware", + "MemoryInjectionMiddleware", "SurfSenseFilesystemMiddleware", ] diff --git a/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py b/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py new file mode 100644 index 000000000..1768442be --- /dev/null +++ b/surfsense_backend/app/agents/new_chat/middleware/memory_injection.py @@ -0,0 +1,115 @@ +"""Memory injection middleware for the SurfSense agent. + +Loads the user's personal memory (User.memory_md) and, for shared threads, +the team memory (SearchSpace.shared_memory_md) from the database and injects +them into the system prompt as / XML blocks on +every turn. This ensures the LLM always has the full memory context without +requiring a tool call. +""" + +from __future__ import annotations + +import logging +from typing import Any +from uuid import UUID + +from langchain.agents.middleware import AgentMiddleware, AgentState +from langchain_core.messages import HumanMessage, SystemMessage +from langgraph.runtime import Runtime +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db import ChatVisibility, SearchSpace, User, shielded_async_session +from app.agents.new_chat.tools.update_memory import MEMORY_HARD_LIMIT + +logger = logging.getLogger(__name__) + + +class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] + """Injects memory markdown into the conversation on every turn.""" + + tools = () + + def __init__( + self, + *, + user_id: str | UUID | None, + search_space_id: int, + thread_visibility: ChatVisibility | None = None, + ) -> None: + self.user_id = UUID(user_id) if isinstance(user_id, str) else user_id + self.search_space_id = search_space_id + self.visibility = thread_visibility or ChatVisibility.PRIVATE + + async def abefore_agent( # type: ignore[override] + self, + state: AgentState, + runtime: Runtime[Any], + ) -> dict[str, Any] | None: + del runtime + messages = state.get("messages") or [] + if not messages: + return None + + last_message = messages[-1] + if not isinstance(last_message, HumanMessage): + return None + + memory_blocks: list[str] = [] + + async with shielded_async_session() as session: + if self.user_id is not None: + user_memory = await self._load_user_memory(session) + if user_memory: + chars = len(user_memory) + memory_blocks.append( + f'\n' + f"{user_memory}\n" + f"" + ) + + if self.visibility == ChatVisibility.SEARCH_SPACE: + team_memory = await self._load_team_memory(session) + if team_memory: + chars = len(team_memory) + memory_blocks.append( + f'\n' + f"{team_memory}\n" + f"" + ) + + if not memory_blocks: + return None + + memory_text = "\n\n".join(memory_blocks) + memory_msg = SystemMessage(content=memory_text) + + new_messages = list(messages) + insert_idx = 1 if len(new_messages) > 1 else 0 + new_messages.insert(insert_idx, memory_msg) + + return {"messages": new_messages} + + async def _load_user_memory(self, session: AsyncSession) -> str | None: + try: + result = await session.execute( + select(User.memory_md).where(User.id == self.user_id) + ) + row = result.scalar_one_or_none() + return row if row else None + except Exception: + logger.exception("Failed to load user memory") + return None + + async def _load_team_memory(self, session: AsyncSession) -> str | None: + try: + result = await session.execute( + select(SearchSpace.shared_memory_md).where( + SearchSpace.id == self.search_space_id + ) + ) + row = result.scalar_one_or_none() + return row if row else None + except Exception: + logger.exception("Failed to load team memory") + return None From 6fc941e4c53a520db2f36d24b1d7d4dfb0ef8444 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:29:26 +0530 Subject: [PATCH 04/41] refactor: rename and consolidate memory tools to update_memory --- .../app/agents/new_chat/tools/__init__.py | 9 +++-- .../app/agents/new_chat/tools/registry.py | 35 ++++--------------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/tools/__init__.py b/surfsense_backend/app/agents/new_chat/tools/__init__.py index a46fadf11..bc444b0c0 100644 --- a/surfsense_backend/app/agents/new_chat/tools/__init__.py +++ b/surfsense_backend/app/agents/new_chat/tools/__init__.py @@ -10,8 +10,7 @@ Available tools: - generate_video_presentation: Generate video presentations with slides and narration - generate_image: Generate images from text descriptions using AI models - scrape_webpage: Extract content from webpages -- save_memory: Store facts/preferences about the user -- recall_memory: Retrieve relevant user memories +- update_memory: Update the user's / team's memory document """ # Registry exports @@ -33,7 +32,7 @@ from .registry import ( ) from .scrape_webpage import create_scrape_webpage_tool from .search_surfsense_docs import create_search_surfsense_docs_tool -from .user_memory import create_recall_memory_tool, create_save_memory_tool +from .update_memory import create_update_memory_tool, create_update_team_memory_tool from .video_presentation import create_generate_video_presentation_tool __all__ = [ @@ -47,10 +46,10 @@ __all__ = [ "create_generate_image_tool", "create_generate_podcast_tool", "create_generate_video_presentation_tool", - "create_recall_memory_tool", - "create_save_memory_tool", "create_scrape_webpage_tool", "create_search_surfsense_docs_tool", + "create_update_memory_tool", + "create_update_team_memory_tool", "format_documents_for_context", "get_all_tool_names", "get_default_enabled_tools", diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py index 442f3ba35..17f962141 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -94,11 +94,7 @@ from .podcast import create_generate_podcast_tool from .report import create_generate_report_tool from .scrape_webpage import create_scrape_webpage_tool from .search_surfsense_docs import create_search_surfsense_docs_tool -from .shared_memory import ( - create_recall_shared_memory_tool, - create_save_shared_memory_tool, -) -from .user_memory import create_recall_memory_tool, create_save_memory_tool +from .update_memory import create_update_memory_tool, create_update_team_memory_tool from .video_presentation import create_generate_video_presentation_tool from .web_search import create_web_search_tool @@ -214,38 +210,19 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ requires=["db_session"], ), # ========================================================================= - # USER MEMORY TOOLS - private or team store by thread_visibility + # MEMORY TOOL - single update_memory, private or team by thread_visibility # ========================================================================= ToolDefinition( - name="save_memory", - description="Save facts, preferences, or context for personalized or team responses", + name="update_memory", + description="Update the memory document (personal or team) with curated long-term information", factory=lambda deps: ( - create_save_shared_memory_tool( - search_space_id=deps["search_space_id"], - created_by_id=deps["user_id"], - db_session=deps["db_session"], - ) - if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE - else create_save_memory_tool( - user_id=deps["user_id"], - search_space_id=deps["search_space_id"], - db_session=deps["db_session"], - ) - ), - requires=["user_id", "search_space_id", "db_session", "thread_visibility"], - ), - ToolDefinition( - name="recall_memory", - description="Recall relevant memories (personal or team) for context", - factory=lambda deps: ( - create_recall_shared_memory_tool( + create_update_team_memory_tool( search_space_id=deps["search_space_id"], db_session=deps["db_session"], ) if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE - else create_recall_memory_tool( + else create_update_memory_tool( user_id=deps["user_id"], - search_space_id=deps["search_space_id"], db_session=deps["db_session"], ) ), From dec381d87ed362f121e3d6357adc05db87f9bb10 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:37:23 +0530 Subject: [PATCH 05/41] feat: add shared_memory_md field and enforce character limit in search space updates --- surfsense_backend/app/routes/__init__.py | 2 ++ surfsense_backend/app/routes/search_spaces_routes.py | 8 ++++++++ surfsense_backend/app/schemas/search_space.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 02367606b..5e3c84c8c 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -29,6 +29,7 @@ from .jira_add_connector_route import router as jira_add_connector_router from .linear_add_connector_route import router as linear_add_connector_router from .logs_routes import router as logs_router from .luma_add_connector_route import router as luma_add_connector_router +from .memory_routes import router as memory_router from .model_list_routes import router as model_list_router from .new_chat_routes import router as new_chat_router from .new_llm_config_routes import router as new_llm_config_router @@ -98,4 +99,5 @@ router.include_router(incentive_tasks_router) # Incentive tasks for earning fre router.include_router(stripe_router) # Stripe checkout for additional page packs router.include_router(youtube_router) # YouTube playlist resolution router.include_router(prompts_router) +router.include_router(memory_router) # User personal memory (memory.md style) router.include_router(autocomplete_router) # Lightweight autocomplete with KB context diff --git a/surfsense_backend/app/routes/search_spaces_routes.py b/surfsense_backend/app/routes/search_spaces_routes.py index 78be97aa1..6fd92fd18 100644 --- a/surfsense_backend/app/routes/search_spaces_routes.py +++ b/surfsense_backend/app/routes/search_spaces_routes.py @@ -5,6 +5,7 @@ from sqlalchemy import func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from app.agents.new_chat.tools.update_memory import MEMORY_HARD_LIMIT from app.config import config from app.db import ( ImageGenerationConfig, @@ -255,6 +256,13 @@ async def update_search_space( raise HTTPException(status_code=404, detail="Search space not found") update_data = search_space_update.model_dump(exclude_unset=True) + + if "shared_memory_md" in update_data and len(update_data["shared_memory_md"] or "") > MEMORY_HARD_LIMIT: + raise HTTPException( + status_code=400, + detail=f"Team memory exceeds {MEMORY_HARD_LIMIT:,} character limit.", + ) + for key, value in update_data.items(): setattr(db_search_space, key, value) await session.commit() diff --git a/surfsense_backend/app/schemas/search_space.py b/surfsense_backend/app/schemas/search_space.py index 054fe1465..e3b50be65 100644 --- a/surfsense_backend/app/schemas/search_space.py +++ b/surfsense_backend/app/schemas/search_space.py @@ -21,6 +21,7 @@ class SearchSpaceUpdate(BaseModel): description: str | None = None citations_enabled: bool | None = None qna_custom_instructions: str | None = None + shared_memory_md: str | None = None class SearchSpaceRead(SearchSpaceBase, IDModel, TimestampModel): @@ -29,6 +30,7 @@ class SearchSpaceRead(SearchSpaceBase, IDModel, TimestampModel): user_id: uuid.UUID citations_enabled: bool qna_custom_instructions: str | None = None + shared_memory_md: str | None = None model_config = ConfigDict(from_attributes=True) From dfa6005af52ec2b1d7e13438167d600b64cd9b8e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:54:29 +0530 Subject: [PATCH 06/41] feat: implement update_memory tool and routes for user memory management --- .../agents/new_chat/tools/update_memory.py | 157 ++++++++++++++++++ surfsense_backend/app/routes/memory_routes.py | 46 +++++ 2 files changed, 203 insertions(+) create mode 100644 surfsense_backend/app/agents/new_chat/tools/update_memory.py create mode 100644 surfsense_backend/app/routes/memory_routes.py diff --git a/surfsense_backend/app/agents/new_chat/tools/update_memory.py b/surfsense_backend/app/agents/new_chat/tools/update_memory.py new file mode 100644 index 000000000..1bb51b94f --- /dev/null +++ b/surfsense_backend/app/agents/new_chat/tools/update_memory.py @@ -0,0 +1,157 @@ +"""Markdown-document memory tool for the SurfSense agent. + +Replaces the old row-per-fact save_memory / recall_memory tools with a single +update_memory tool that overwrites a freeform markdown TEXT column. The LLM +always sees the current memory in / tags injected +by MemoryInjectionMiddleware, so it passes the FULL updated document each time. +""" + +from __future__ import annotations + +import logging +from typing import Any +from uuid import UUID + +from langchain_core.tools import tool +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db import SearchSpace, User + +logger = logging.getLogger(__name__) + +MEMORY_SOFT_LIMIT = 20_000 +MEMORY_HARD_LIMIT = 25_000 + + +def _validate_memory_size(content: str) -> dict[str, Any] | None: + """Return an error/warning dict if *content* is too large, else None.""" + length = len(content) + if length > MEMORY_HARD_LIMIT: + return { + "status": "error", + "message": ( + f"Memory exceeds {MEMORY_HARD_LIMIT:,} character limit " + f"({length:,} chars). Consolidate by merging related items, " + "removing outdated entries, and shortening descriptions. " + "Then call update_memory again." + ), + } + return None + + +def _soft_warning(content: str) -> str | None: + """Return a warning string if content exceeds the soft limit.""" + length = len(content) + if length > MEMORY_SOFT_LIMIT: + return ( + f"Memory is at {length:,}/{MEMORY_HARD_LIMIT:,} characters. " + "Consolidate by merging related items and removing less important " + "entries on your next update." + ) + return None + + +def create_update_memory_tool( + user_id: str | UUID, + db_session: AsyncSession, +): + uid = UUID(user_id) if isinstance(user_id, str) else user_id + + @tool + async def update_memory(updated_memory: str) -> dict[str, Any]: + """Update the user's personal memory document. + + Your current memory is shown in in the system prompt. + When the user shares important long-term information (preferences, + facts, instructions, context), rewrite the memory document to include + the new information. Merge new facts with existing ones, update + contradictions, remove outdated entries, and keep it concise. + + Args: + updated_memory: The FULL updated markdown document (not a diff). + """ + error = _validate_memory_size(updated_memory) + if error: + return error + + try: + result = await db_session.execute( + select(User).where(User.id == uid) + ) + user = result.scalars().first() + if not user: + return {"status": "error", "message": "User not found."} + + user.memory_md = updated_memory + await db_session.commit() + + resp: dict[str, Any] = { + "status": "saved", + "message": "Memory updated.", + } + warning = _soft_warning(updated_memory) + if warning: + resp["warning"] = warning + return resp + except Exception as e: + logger.exception("Failed to update user memory: %s", e) + await db_session.rollback() + return { + "status": "error", + "message": f"Failed to update memory: {e}", + } + + return update_memory + + +def create_update_team_memory_tool( + search_space_id: int, + db_session: AsyncSession, +): + @tool + async def update_memory(updated_memory: str) -> dict[str, Any]: + """Update the team's shared memory document for this search space. + + Your current team memory is shown in in the system + prompt. When the team shares important long-term information + (decisions, conventions, key facts, priorities), rewrite the memory + document to include the new information. Merge new facts with + existing ones, update contradictions, remove outdated entries, and + keep it concise. + + Args: + updated_memory: The FULL updated markdown document (not a diff). + """ + error = _validate_memory_size(updated_memory) + if error: + return error + + try: + result = await db_session.execute( + select(SearchSpace).where(SearchSpace.id == search_space_id) + ) + space = result.scalars().first() + if not space: + return {"status": "error", "message": "Search space not found."} + + space.shared_memory_md = updated_memory + await db_session.commit() + + resp: dict[str, Any] = { + "status": "saved", + "message": "Team memory updated.", + } + warning = _soft_warning(updated_memory) + if warning: + resp["warning"] = warning + return resp + except Exception as e: + logger.exception("Failed to update team memory: %s", e) + await db_session.rollback() + return { + "status": "error", + "message": f"Failed to update team memory: {e}", + } + + return update_memory diff --git a/surfsense_backend/app/routes/memory_routes.py b/surfsense_backend/app/routes/memory_routes.py new file mode 100644 index 000000000..aa8b1be28 --- /dev/null +++ b/surfsense_backend/app/routes/memory_routes.py @@ -0,0 +1,46 @@ +"""Routes for user memory management (personal memory.md).""" + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +from app.agents.new_chat.tools.update_memory import MEMORY_HARD_LIMIT +from app.db import User, get_async_session +from app.users import current_active_user + +router = APIRouter() + + +class MemoryRead(BaseModel): + memory_md: str + + +class MemoryUpdate(BaseModel): + memory_md: str + + +@router.get("/users/me/memory", response_model=MemoryRead) +async def get_user_memory( + user: User = Depends(current_active_user), + session: AsyncSession = Depends(get_async_session), +): + await session.refresh(user, ["memory_md"]) + return MemoryRead(memory_md=user.memory_md or "") + + +@router.put("/users/me/memory", response_model=MemoryRead) +async def update_user_memory( + body: MemoryUpdate, + user: User = Depends(current_active_user), + session: AsyncSession = Depends(get_async_session), +): + if len(body.memory_md) > MEMORY_HARD_LIMIT: + raise HTTPException( + status_code=400, + detail=f"Memory exceeds {MEMORY_HARD_LIMIT:,} character limit ({len(body.memory_md):,} chars).", + ) + user.memory_md = body.memory_md + session.add(user) + await session.commit() + await session.refresh(user, ["memory_md"]) + return MemoryRead(memory_md=user.memory_md or "") From c358cedbaee6dbe8a448273b3a3eb8a8568ca1e8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:00:39 +0530 Subject: [PATCH 07/41] feat: add chat session and message synchronization hooks --- .../new-chat/[[...chat_id]]/page.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 7bef1fff2..e92134111 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -38,6 +38,43 @@ import { removeChatTabAtom, updateChatTabTitleAtom } from "@/atoms/tabs/tabs.ato import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { ThinkingStepsDataUI } from "@/components/assistant-ui/thinking-steps"; import { Thread } from "@/components/assistant-ui/thread"; +import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; +import { useMessagesSync } from "@/hooks/use-messages-sync"; +import { getBearerToken } from "@/lib/auth-utils"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; +import { + type ContentPartsState, + FrameBatchedUpdater, + type ThinkingStepData, + addToolCall, + appendText, + buildContentForPersistence, + buildContentForUI, + readSSEStream, + updateThinkingSteps, + updateToolCall, +} from "@/lib/chat/streaming-state"; +import { convertToThreadMessage } from "@/lib/chat/message-utils"; +import { + isPodcastGenerating, + looksLikePodcastRequest, + setActivePodcastTaskId, +} from "@/lib/chat/podcast-state"; +import { + type ThreadRecord, + appendMessage, + createThread, + getRegenerateUrl, + getThreadFull, + getThreadMessages, +} from "@/lib/chat/thread-persistence"; +import { NotFoundError } from "@/lib/error"; +import { + trackChatCreated, + trackChatError, + trackChatMessageSent, + trackChatResponseReceived, +} from "@/lib/posthog/events"; import Loading from "../loading"; const MobileEditorPanel = dynamic( From b8de7be9aa4aa01dd57056b45a84052ea06b7b45 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:02:14 +0530 Subject: [PATCH 08/41] refactor: update memory tools in assistant message component --- .../components/assistant-ui/assistant-message.tsx | 11 +++-------- surfsense_web/components/assistant-ui/thread.tsx | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index 626e6237f..cac8b3ae3 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -66,12 +66,8 @@ const GenerateImageToolUI = dynamic( () => import("@/components/tool-ui/generate-image").then(m => ({ default: m.GenerateImageToolUI })), { ssr: false } ); -const SaveMemoryToolUI = dynamic( - () => import("@/components/tool-ui/user-memory").then(m => ({ default: m.SaveMemoryToolUI })), - { ssr: false } -); -const RecallMemoryToolUI = dynamic( - () => import("@/components/tool-ui/user-memory").then(m => ({ default: m.RecallMemoryToolUI })), +const UpdateMemoryToolUI = dynamic( + () => import("@/components/tool-ui/user-memory").then(m => ({ default: m.UpdateMemoryToolUI })), { ssr: false } ); const SandboxExecuteToolUI = dynamic( @@ -345,8 +341,7 @@ const AssistantMessageInner: FC = () => { generate_video_presentation: GenerateVideoPresentationToolUI, display_image: GenerateImageToolUI, generate_image: GenerateImageToolUI, - save_memory: SaveMemoryToolUI, - recall_memory: RecallMemoryToolUI, + update_memory: UpdateMemoryToolUI, execute: SandboxExecuteToolUI, create_notion_page: CreateNotionPageToolUI, update_notion_page: UpdateNotionPageToolUI, diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index e1351cb18..b0d3ee73b 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -1297,7 +1297,7 @@ const TOOL_GROUPS: ToolGroup[] = [ }, { label: "Memory", - tools: ["save_memory", "recall_memory"], + tools: ["update_memory"], }, { label: "Gmail", From e21582f2590aa452fbab0ccf3a20a0f0acc88702 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:02:54 +0530 Subject: [PATCH 09/41] refactor: rename and update memory tool components to reflect new update_memory functionality --- surfsense_web/components/tool-ui/index.ts | 16 +- .../components/tool-ui/user-memory.tsx | 236 ++---------------- surfsense_web/contracts/enums/toolIcons.tsx | 3 +- .../contracts/types/search-space.types.ts | 2 + 4 files changed, 33 insertions(+), 224 deletions(-) diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 6371554ae..4d885a38c 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -51,17 +51,11 @@ export { SandboxExecuteToolUI, } from "./sandbox-execute"; export { - type MemoryItem, - type RecallMemoryArgs, - RecallMemoryArgsSchema, - type RecallMemoryResult, - RecallMemoryResultSchema, - RecallMemoryToolUI, - type SaveMemoryArgs, - SaveMemoryArgsSchema, - type SaveMemoryResult, - SaveMemoryResultSchema, - SaveMemoryToolUI, + type UpdateMemoryArgs, + UpdateMemoryArgsSchema, + type UpdateMemoryResult, + UpdateMemoryResultSchema, + UpdateMemoryToolUI, } from "./user-memory"; export { GenerateVideoPresentationToolUI } from "./video-presentation"; export { type WriteTodosData, WriteTodosSchema, WriteTodosToolUI } from "./write-todos"; diff --git a/surfsense_web/components/tool-ui/user-memory.tsx b/surfsense_web/components/tool-ui/user-memory.tsx index e232bdcc7..800b9e601 100644 --- a/surfsense_web/components/tool-ui/user-memory.tsx +++ b/surfsense_web/components/tool-ui/user-memory.tsx @@ -1,100 +1,38 @@ "use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; -import { BrainIcon, CheckIcon, Loader2Icon, SearchIcon, XIcon } from "lucide-react"; +import { AlertTriangleIcon, BrainIcon, CheckIcon, Loader2Icon, XIcon } from "lucide-react"; import { z } from "zod"; // ============================================================================ -// Zod Schemas for save_memory tool +// Zod Schemas for update_memory tool // ============================================================================ -const SaveMemoryArgsSchema = z.object({ - content: z.string(), - category: z.string().default("fact"), +const UpdateMemoryArgsSchema = z.object({ + updated_memory: z.string(), }); -const SaveMemoryResultSchema = z.object({ +const UpdateMemoryResultSchema = z.object({ status: z.enum(["saved", "error"]), - memory_id: z.number().nullish(), - memory_text: z.string().nullish(), - category: z.string().nullish(), message: z.string().nullish(), - error: z.string().nullish(), + warning: z.string().nullish(), }); -type SaveMemoryArgs = z.infer; -type SaveMemoryResult = z.infer; +type UpdateMemoryArgs = z.infer; +type UpdateMemoryResult = z.infer; // ============================================================================ -// Zod Schemas for recall_memory tool +// Update Memory Tool UI // ============================================================================ -const RecallMemoryArgsSchema = z.object({ - query: z.string().nullish(), - category: z.string().nullish(), - top_k: z.number().default(5), -}); - -const MemoryItemSchema = z.object({ - id: z.number(), - memory_text: z.string(), - category: z.string(), - updated_at: z.string().nullish(), -}); - -const RecallMemoryResultSchema = z.object({ - status: z.enum(["success", "error"]), - count: z.number().nullish(), - memories: z.array(MemoryItemSchema).nullish(), - formatted_context: z.string().nullish(), - error: z.string().nullish(), -}); - -type RecallMemoryArgs = z.infer; -type RecallMemoryResult = z.infer; -type MemoryItem = z.infer; - -// ============================================================================ -// Category badge colors -// ============================================================================ - -const categoryColors: Record = { - preference: "bg-blue-500/10 text-blue-600 dark:text-blue-400", - fact: "bg-green-500/10 text-green-600 dark:text-green-400", - instruction: "bg-purple-500/10 text-purple-600 dark:text-purple-400", - context: "bg-orange-500/10 text-orange-600 dark:text-orange-400", -}; - -function CategoryBadge({ category }: { category: string }) { - const colorClass = categoryColors[category] || "bg-gray-500/10 text-gray-600 dark:text-gray-400"; - return ( - - {category} - - ); -} - -// ============================================================================ -// Save Memory Tool UI -// ============================================================================ - -export const SaveMemoryToolUI = ({ - args, +export const UpdateMemoryToolUI = ({ result, status, -}: ToolCallMessagePartProps) => { +}: ToolCallMessagePartProps) => { const isRunning = status.type === "running" || status.type === "requires-action"; const isComplete = status.type === "complete"; const isError = result?.status === "error"; - // Parse args safely - const parsedArgs = SaveMemoryArgsSchema.safeParse(args); - const content = parsedArgs.success ? parsedArgs.data.content : ""; - const category = parsedArgs.success ? parsedArgs.data.category : "fact"; - - // Loading state if (isRunning) { return (
@@ -102,13 +40,12 @@ export const SaveMemoryToolUI = ({
- Saving to memory... + Updating memory...
); } - // Error state if (isError) { return (
@@ -116,14 +53,15 @@ export const SaveMemoryToolUI = ({
- Failed to save memory - {result?.error &&

{result.error}

} + Failed to update memory + {result?.message && ( +

{result.message}

+ )}
); } - // Success state if (isComplete && result?.status === "saved") { return (
@@ -133,138 +71,19 @@ export const SaveMemoryToolUI = ({
- Memory saved - + Memory updated
-

{content}

-
-
- ); - } - - // Default/incomplete state - show what's being saved - if (content) { - return ( -
-
- -
-
-
- Saving memory - -
-

{content}

-
-
- ); - } - - return null; -}; - -// ============================================================================ -// Recall Memory Tool UI -// ============================================================================ - -export const RecallMemoryToolUI = ({ - args, - result, - status, -}: ToolCallMessagePartProps) => { - const isRunning = status.type === "running" || status.type === "requires-action"; - const isComplete = status.type === "complete"; - const isError = result?.status === "error"; - - // Parse args safely - const parsedArgs = RecallMemoryArgsSchema.safeParse(args); - const query = parsedArgs.success ? parsedArgs.data.query : null; - - // Loading state - if (isRunning) { - return ( -
-
- -
-
- - {query ? `Searching memories for "${query}"...` : "Recalling memories..."} - -
-
- ); - } - - // Error state - if (isError) { - return ( -
-
- -
-
- Failed to recall memories - {result?.error &&

{result.error}

} -
-
- ); - } - - // Success state with memories - if (isComplete && result?.status === "success") { - const memories = result.memories || []; - const count = result.count || 0; - - if (count === 0) { - return ( -
-
- -
- No memories found -
- ); - } - - return ( -
-
- - - Recalled {count} {count === 1 ? "memory" : "memories"} - -
-
- {memories.slice(0, 5).map((memory: MemoryItem) => ( -
- - {memory.memory_text} + {result.warning && ( +
+ +

{result.warning}

- ))} - {memories.length > 5 && ( -

...and {memories.length - 5} more

)}
); } - // Default/incomplete state - if (query) { - return ( -
-
- -
- Searching memories for "{query}" -
- ); - } - return null; }; @@ -273,13 +92,8 @@ export const RecallMemoryToolUI = ({ // ============================================================================ export { - SaveMemoryArgsSchema, - SaveMemoryResultSchema, - RecallMemoryArgsSchema, - RecallMemoryResultSchema, - type SaveMemoryArgs, - type SaveMemoryResult, - type RecallMemoryArgs, - type RecallMemoryResult, - type MemoryItem, + UpdateMemoryArgsSchema, + UpdateMemoryResultSchema, + type UpdateMemoryArgs, + type UpdateMemoryResult, }; diff --git a/surfsense_web/contracts/enums/toolIcons.tsx b/surfsense_web/contracts/enums/toolIcons.tsx index 8e5d1e452..fd12aaa9c 100644 --- a/surfsense_web/contracts/enums/toolIcons.tsx +++ b/surfsense_web/contracts/enums/toolIcons.tsx @@ -19,8 +19,7 @@ const TOOL_ICONS: Record = { scrape_webpage: ScanLine, web_search: Globe, search_surfsense_docs: BookOpen, - save_memory: Brain, - recall_memory: Brain, + update_memory: Brain, }; export function getToolIcon(name: string): LucideIcon { diff --git a/surfsense_web/contracts/types/search-space.types.ts b/surfsense_web/contracts/types/search-space.types.ts index 8a0a2fb4c..7b4fefb62 100644 --- a/surfsense_web/contracts/types/search-space.types.ts +++ b/surfsense_web/contracts/types/search-space.types.ts @@ -9,6 +9,7 @@ export const searchSpace = z.object({ user_id: z.string(), citations_enabled: z.boolean(), qna_custom_instructions: z.string().nullable(), + shared_memory_md: z.string().nullable().optional(), member_count: z.number(), is_owner: z.boolean(), }); @@ -54,6 +55,7 @@ export const updateSearchSpaceRequest = z.object({ description: true, citations_enabled: true, qna_custom_instructions: true, + shared_memory_md: true, }) .partial(), }); From 3ea9b300468f750a17c76e2d6153fd4581b814b8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:03:41 +0530 Subject: [PATCH 10/41] feat: add MemoryContent and TeamMemoryManager components for user and team memory management --- .../components/MemoryContent.tsx | 145 +++++++++++++++++ .../settings/search-space-settings-dialog.tsx | 12 +- .../settings/team-memory-manager.tsx | 151 ++++++++++++++++++ .../settings/user-settings-dialog.tsx | 28 ++-- 4 files changed, 326 insertions(+), 10 deletions(-) create mode 100644 surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx create mode 100644 surfsense_web/components/settings/team-memory-manager.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx new file mode 100644 index 000000000..007f45feb --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Spinner } from "@/components/ui/spinner"; +import { Textarea } from "@/components/ui/textarea"; +import { baseApiService } from "@/lib/apis/base-api.service"; + +const MEMORY_HARD_LIMIT = 25_000; + +const MemoryReadSchema = z.object({ + memory_md: z.string(), +}); + +export function MemoryContent() { + const [memory, setMemory] = useState(""); + const [savedMemory, setSavedMemory] = useState(""); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const fetchMemory = useCallback(async () => { + try { + setLoading(true); + const data = await baseApiService.get("/api/v1/users/me/memory", MemoryReadSchema); + setMemory(data.memory_md); + setSavedMemory(data.memory_md); + } catch { + toast.error("Failed to load memory"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchMemory(); + }, [fetchMemory]); + + const handleSave = async () => { + try { + setSaving(true); + const data = await baseApiService.put("/api/v1/users/me/memory", MemoryReadSchema, { + body: { memory_md: memory }, + }); + setSavedMemory(data.memory_md); + toast.success("Memory saved"); + } catch { + toast.error("Failed to save memory"); + } finally { + setSaving(false); + } + }; + + const handleClear = async () => { + try { + setSaving(true); + const data = await baseApiService.put("/api/v1/users/me/memory", MemoryReadSchema, { + body: { memory_md: "" }, + }); + setMemory(data.memory_md); + setSavedMemory(data.memory_md); + toast.success("Memory cleared"); + } catch { + toast.error("Failed to clear memory"); + } finally { + setSaving(false); + } + }; + + const hasChanges = memory !== savedMemory; + const charCount = memory.length; + const isOverLimit = charCount > MEMORY_HARD_LIMIT; + + const getCounterColor = () => { + if (charCount > MEMORY_HARD_LIMIT) return "text-red-500"; + if (charCount > 20_000) return "text-orange-500"; + if (charCount > 15_000) return "text-yellow-500"; + return "text-muted-foreground"; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+ +

+ This is your personal memory document. The AI assistant reads it at the start of + every conversation and uses it to personalize responses. You can edit it directly + or let the assistant update it during conversations. +

+
+ +