diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a6c8e4a..2efaad25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,9 @@ name: Tests on: push: - branches: [ main, master, dev, re-org, production ] + branches: [ main, master, dev, server, re-org, production ] pull_request: - branches: [ main, master, dev, re-org, production ] + branches: [ main, master, dev, server, re-org, production ] jobs: test: @@ -46,6 +46,7 @@ jobs: run: | cd "${{ github.workspace }}" python -m pytest tests/test_memory_server.py \ + tests/test_credit_system.py \ tests/test_auth_provider.py \ tests/test_queue.py \ tests/test_redis_integration.py -v diff --git a/README.md b/README.md index 590a28aa..4b272a9d 100755 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ Your personal AI that builds memory through screen observation and natural conversation + + + + +
+ Important Update: 0.1.4 (Main) vs 0.1.3 (Desktop Agent)
+ Starting with 0.1.4, the main branch is a brand-new release line where Mirix is a pure memory system that can be plugged into any existing agents. The desktop personal assistant (frontend + backend) has been deprecated and is no longer shipped on main. If you need the earlier desktop application with the built-in agent, use the desktop-agent branch. +
+ | 🌐 [Website](https://mirix.io) | 📚 [Documentation](https://docs.mirix.io) | 📄 [Paper](https://arxiv.org/abs/2507.07957) | 💬 [Discord](https://discord.gg/S6CeHNrJ) @@ -11,7 +20,7 @@ Your personal AI that builds memory through screen observation and natural conve ### Key Features 🔥 -- **Multi-Agent Memory System:** Six specialized memory components (Core, Episodic, Semantic, Procedural, Resource, Knowledge Vault) managed by dedicated agents +- **Multi-Agent Memory System:** Six specialized memory components (Core, Episodic, Semantic, Procedural, Resource, Knowledge) managed by dedicated agents - **Screen Activity Tracking:** Continuous visual data capture and intelligent consolidation into structured memories - **Privacy-First Design:** All long-term data stored locally with user-controlled privacy settings - **Advanced Search:** PostgreSQL-native BM25 full-text search with vector similarity support @@ -44,35 +53,40 @@ client = MirixClient( client.initialize_meta_agent( config={ "llm_config": { - "model": "gemini-2.0-flash", - "model_endpoint_type": "google_ai", - "api_key": "your-api-key-here", - "model_endpoint": "https://generativelanguage.googleapis.com", - "context_window": 1_000_000, + "model": "gpt-4o-mini", + "model_endpoint_type": "openai", + "model_endpoint": "https://api.openai.com/v1", + "context_window": 128000, }, + "build_embeddings_for_memory": True, "embedding_config": { - "embedding_model": "text-embedding-004", - "embedding_endpoint_type": "google_ai", - "api_key": "your-api-key-here", - "embedding_endpoint": "https://generativelanguage.googleapis.com", - "embedding_dim": 768, + "embedding_model": "text-embedding-3-small", + "embedding_endpoint": "https://api.openai.com/v1", + "embedding_endpoint_type": "openai", + "embedding_dim": 1536, }, "meta_agent_config": { + "system_prompts_folder": "mirix\\prompts\\system\\base", "agents": [ - { - "core_memory_agent": { - "blocks": [ - {"label": "human", "value": ""}, - {"label": "persona", "value": "I am a helpful assistant."}, - ] - } - }, + "core_memory_agent", "resource_memory_agent", "semantic_memory_agent", "episodic_memory_agent", "procedural_memory_agent", - "knowledge_vault_memory_agent", + "knowledge_memory_agent", + "reflexion_agent", + "background_agent", ], + "memory": { + "core": [ + {"label": "human", "value": ""}, + {"label": "persona", "value": "I am a helpful assistant."}, + ], + "decay": { + "fade_after_days": 30, + "expire_after_days": 90, + }, + }, }, } ) @@ -110,7 +124,7 @@ Connect with other Mirix users, share your thoughts, and get support: ### 💬 Discord Community Join our Discord server for real-time discussions, support, and community updates: -**[https://discord.gg/S6CeHNrJ](https://discord.gg/S6CeHNrJ)** +**[https://discord.gg/FXtXJuRf](https://discord.gg/FXtXJuRf)** ### 🎯 Weekly Discussion Sessions We host weekly discussion sessions where you can: diff --git a/client_env/Scripts/python.exe b/client_env/Scripts/python.exe new file mode 100644 index 00000000..24f36b79 Binary files /dev/null and b/client_env/Scripts/python.exe differ diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 53a0b4c3..9c197499 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -82,6 +82,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1920,6 +1921,7 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1937,6 +1939,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1948,6 +1951,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -1992,6 +1996,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2179,6 +2184,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2424,6 +2430,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2874,6 +2881,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3660,6 +3668,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -4185,6 +4194,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4380,6 +4390,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4392,6 +4403,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4918,6 +4930,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4996,6 +5009,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5108,6 +5122,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/dashboard/src/components/UserSwitcher.tsx b/dashboard/src/components/UserSwitcher.tsx index 11177a64..559c4b9a 100644 --- a/dashboard/src/components/UserSwitcher.tsx +++ b/dashboard/src/components/UserSwitcher.tsx @@ -34,7 +34,7 @@ export const UserSwitcher: React.FC<{ className?: string }> = ({ className }) => // Check if a user is the default user const isDefaultUser = (user: User) => { - return clientUser?.default_user_id === user.id; + return clientUser?.admin_user_id === user.id; }; const handleCreateUser = async (e: React.FormEvent) => { diff --git a/dashboard/src/components/ui/badge.tsx b/dashboard/src/components/ui/badge.tsx new file mode 100644 index 00000000..bf138e25 --- /dev/null +++ b/dashboard/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } + diff --git a/dashboard/src/components/ui/separator.tsx b/dashboard/src/components/ui/separator.tsx new file mode 100644 index 00000000..0d4eee80 --- /dev/null +++ b/dashboard/src/components/ui/separator.tsx @@ -0,0 +1,21 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { orientation?: "horizontal" | "vertical" } +>(({ className, orientation = "horizontal", ...props }, ref) => ( +
+)) +Separator.displayName = "Separator" + +export { Separator } + diff --git a/dashboard/src/contexts/AuthContext.tsx b/dashboard/src/contexts/AuthContext.tsx index dcc337e6..3633a788 100644 --- a/dashboard/src/contexts/AuthContext.tsx +++ b/dashboard/src/contexts/AuthContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import apiClient from '@/api/client'; import { useNavigate } from 'react-router-dom'; @@ -8,8 +8,9 @@ interface ClientUser { email: string; scope: string; status: string; - default_user_id: string; // Default user for memory operations + admin_user_id: string; // Default user for memory operations created_at: string; + credits: number; // Available credits for LLM API calls } interface AuthContextType { @@ -17,6 +18,7 @@ interface AuthContextType { token: string | null; login: (token: string, user: ClientUser) => void; logout: () => void; + refreshUser: () => Promise; // Refresh user data from server isAuthenticated: boolean; isLoading: boolean; } @@ -70,8 +72,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children navigate('/login'); }; + const refreshUser = useCallback(async () => { + const storedToken = localStorage.getItem('token'); + if (!storedToken) return; + + try { + const response = await apiClient.get('/admin/auth/me'); + setUser(response.data); + setToken(storedToken); + } catch (error) { + console.error('Failed to refresh user profile', error); + } + }, []); + return ( - + {children} ); diff --git a/dashboard/src/contexts/WorkspaceContext.tsx b/dashboard/src/contexts/WorkspaceContext.tsx index 62983259..ca0637ff 100644 --- a/dashboard/src/contexts/WorkspaceContext.tsx +++ b/dashboard/src/contexts/WorkspaceContext.tsx @@ -1,4 +1,5 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import apiClient from '@/api/client'; import { useAuth } from '@/contexts/AuthContext'; @@ -32,26 +33,50 @@ export const useWorkspace = () => { }; export const WorkspaceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { user: clientUser } = useAuth(); // This is the Client (Admin) - includes default_user_id + const { user: clientUser } = useAuth(); // This is the Client (Admin) - includes admin_user_id + const [searchParams] = useSearchParams(); const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); const [isLoading, setIsLoading] = useState(true); + const selectedUserRef = useRef(null); + const requestedUserParam = searchParams.get('user') || searchParams.get('user_id'); + + useEffect(() => { + selectedUserRef.current = selectedUser; + }, [selectedUser]); + + const normalizeUserToken = (value: string) => value.replace(/_/g, '-'); + + const findUserFromParam = (userList: User[], param: string | null) => { + if (!param) return null; + const normalizedParam = normalizeUserToken(param); + return ( + userList.find((user) => user.id === param || user.name === param) || + userList.find((user) => normalizeUserToken(user.id) === normalizedParam) || + userList.find((user) => normalizeUserToken(user.name) === normalizedParam) || + null + ); + }; const fetchUsers = async () => { if (!clientUser) return; setIsLoading(true); try { - // The clientUser now includes default_user_id from the backend - const defaultUserId = clientUser.default_user_id; + // The clientUser now includes admin_user_id from the backend + const defaultUserId = clientUser.admin_user_id; // List all users const response = await apiClient.get('/users'); const userList = response.data; setUsers(userList); - // Auto-select the default user if none is selected yet - if (!selectedUser && userList.length > 0) { - // Find the default user by ID + const matchedUser = findUserFromParam(userList, requestedUserParam); + const currentSelected = selectedUserRef.current; + + if (matchedUser && currentSelected?.id !== matchedUser.id) { + setSelectedUser(matchedUser); + } else if (!currentSelected && userList.length > 0) { + // Auto-select the default user if none is selected yet const defaultUser = userList.find((u: User) => u.id === defaultUserId); if (defaultUser) { setSelectedUser(defaultUser); @@ -90,7 +115,7 @@ export const WorkspaceProvider: React.FC<{ children: React.ReactNode }> = ({ chi const remainingUsers = users.filter(u => u.id !== userId); if (remainingUsers.length > 0) { // Try to select the default user, otherwise first available - const defaultUser = remainingUsers.find(u => u.id === clientUser.default_user_id); + const defaultUser = remainingUsers.find(u => u.id === clientUser.admin_user_id); setSelectedUser(defaultUser || remainingUsers[0]); } else { setSelectedUser(null); @@ -106,6 +131,14 @@ export const WorkspaceProvider: React.FC<{ children: React.ReactNode }> = ({ chi } }, [clientUser]); + useEffect(() => { + if (!requestedUserParam || users.length === 0) return; + const matchedUser = findUserFromParam(users, requestedUserParam); + if (matchedUser && selectedUserRef.current?.id !== matchedUser.id) { + setSelectedUser(matchedUser); + } + }, [requestedUserParam, users]); + return ( { { name: 'Overview', href: '/dashboard', icon: LayoutDashboard, end: true }, { name: 'API Keys', href: '/dashboard/api-keys', icon: Key }, { name: 'Memories', href: '/dashboard/memories', icon: Brain }, - // { name: 'Settings', href: '/dashboard/settings', icon: Settings }, + { name: 'Memory Traces', href: '/dashboard/memory-traces', icon: Activity }, + { name: 'Usage', href: '/dashboard/usage', icon: CreditCard }, ]; return ( @@ -80,6 +83,8 @@ export const Dashboard: React.FC = () => { } /> } /> } /> + } /> + } /> } />
diff --git a/dashboard/src/pages/dashboard/ApiKeys.tsx b/dashboard/src/pages/dashboard/ApiKeys.tsx index d67cc64a..91f66c1f 100644 --- a/dashboard/src/pages/dashboard/ApiKeys.tsx +++ b/dashboard/src/pages/dashboard/ApiKeys.tsx @@ -227,7 +227,7 @@ export const ApiKeys: React.FC = () => { > {users.map((u) => ( ))} diff --git a/dashboard/src/pages/dashboard/Memories.tsx b/dashboard/src/pages/dashboard/Memories.tsx index f1940fe9..de55cf12 100644 --- a/dashboard/src/pages/dashboard/Memories.tsx +++ b/dashboard/src/pages/dashboard/Memories.tsx @@ -23,7 +23,7 @@ type MemoryTab = | 'semantic' | 'procedural' | 'resource' - | 'knowledge_vault' + | 'knowledge' | 'core'; interface MemoryCollection { @@ -71,7 +71,7 @@ interface ResourceMemory { updated_at?: string; } -interface KnowledgeVaultMemory { +interface KnowledgeMemory { id: string; entry_type?: string; source?: string; @@ -93,7 +93,7 @@ type MemoryCollections = { semantic?: MemoryCollection; procedural?: MemoryCollection; resource?: MemoryCollection; - knowledge_vault?: MemoryCollection; + knowledge?: MemoryCollection; core?: MemoryCollection; }; @@ -105,7 +105,7 @@ const DEFAULT_FIELDS: Record = { semantic: ['name', 'summary', 'details'], procedural: ['summary', 'steps'], resource: ['summary', 'content'], - knowledge_vault: ['caption', 'secret_value'], + knowledge: ['caption', 'secret_value'], core: ['label', 'value'], }; @@ -114,14 +114,15 @@ const MEMORY_TABS: { key: MemoryTab; label: string }[] = [ { key: 'semantic', label: 'Semantic' }, { key: 'procedural', label: 'Procedural' }, { key: 'resource', label: 'Resource' }, - { key: 'knowledge_vault', label: 'Knowledge Vault' }, + { key: 'knowledge', label: 'Knowledge' }, { key: 'core', label: 'Core' }, ]; const formatDate = (value?: string) => (value ? new Date(value).toLocaleString() : 'N/A'); +const normalizeUserToken = (value: string) => value.replace(/_/g, '-'); export const Memories: React.FC = () => { - const { selectedUser } = useWorkspace(); + const { users, selectedUser, setSelectedUser, isLoading: usersLoading } = useWorkspace(); const [searchParams, setSearchParams] = useSearchParams(); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); @@ -143,11 +144,25 @@ export const Memories: React.FC = () => { const [expandedKnowledge, setExpandedKnowledge] = useState>({}); const [fieldsByType, setFieldsByType] = useState>(DEFAULT_FIELDS); const prevUserIdRef = useRef(null); + const userParam = searchParams.get('user'); + const searchParamsString = searchParams.toString(); + const userParamAppliedRef = useRef(false); + const lastUserParamRef = useRef(null); + const normalizedUserParam = userParam ? normalizeUserToken(userParam) : null; + const userParamValid = + !!userParam && + users.some( + (user) => + user.id === userParam || + user.name === userParam || + normalizeUserToken(user.id) === normalizedUserParam || + normalizeUserToken(user.name) === normalizedUserParam + ); // Initialize state from URL params useEffect(() => { const typeParam = searchParams.get('type') as MemoryTypeFilter | null; - if (typeParam && (['all', 'episodic', 'semantic', 'procedural', 'resource', 'knowledge_vault', 'core'] as MemoryTypeFilter[]).includes(typeParam)) { + if (typeParam && (['all', 'episodic', 'semantic', 'procedural', 'resource', 'knowledge', 'core'] as MemoryTypeFilter[]).includes(typeParam)) { setMemoryTypeFilter(typeParam); } @@ -162,7 +177,7 @@ export const Memories: React.FC = () => { } const tabParam = searchParams.get('tab') as MemoryTab | null; - if (tabParam && (['episodic', 'semantic', 'procedural', 'resource', 'knowledge_vault', 'core'] as MemoryTab[]).includes(tabParam)) { + if (tabParam && (['episodic', 'semantic', 'procedural', 'resource', 'knowledge', 'core'] as MemoryTab[]).includes(tabParam)) { setMemoryTab(tabParam); } @@ -176,6 +191,7 @@ export const Memories: React.FC = () => { if (modeParam === 'search' || modeParam === 'list') { setViewMode(modeParam); } + }, []); // run once on mount const fetchMemoryComponents = useCallback(async () => { @@ -252,6 +268,27 @@ export const Memories: React.FC = () => { } }, [selectedUser?.id, fetchMemoryComponents]); + useEffect(() => { + if (usersLoading || !userParam || users.length === 0) return; + const matchingUser = + users.find((user) => user.id === userParam || user.name === userParam) || + (normalizedUserParam + ? users.find( + (user) => + normalizeUserToken(user.id) === normalizedUserParam || + normalizeUserToken(user.name) === normalizedUserParam + ) + : null); + const userParamChanged = userParam !== lastUserParamRef.current; + if (matchingUser && (userParamChanged || !userParamAppliedRef.current)) { + if (selectedUser?.id !== matchingUser.id) { + setSelectedUser(matchingUser); + } + userParamAppliedRef.current = true; + } + lastUserParamRef.current = userParam; + }, [usersLoading, userParam, users, selectedUser?.id, setSelectedUser]); + const toggleEpisodic = (id: string) => { setExpandedEpisodic((prev) => ({ ...prev, [id]: !prev[id] })); }; @@ -283,12 +320,12 @@ export const Memories: React.FC = () => { setSearchField(availableFields[0] || 'summary'); return; } - // Avoid invalid combinations: embedding cannot search resource.content or knowledge_vault.secret_value + // Avoid invalid combinations: embedding cannot search resource.content or knowledge.secret_value if (searchMethod === 'embedding') { if (memoryTypeFilter === 'resource' && searchField === 'content') { setSearchField('summary'); } - if (memoryTypeFilter === 'knowledge_vault' && searchField === 'secret_value') { + if (memoryTypeFilter === 'knowledge' && searchField === 'secret_value') { setSearchField('caption'); } } @@ -303,8 +340,26 @@ export const Memories: React.FC = () => { params.set('method', searchMethod); params.set('tab', memoryTab); params.set('mode', viewMode); - setSearchParams(params, { replace: true }); - }, [query, memoryTypeFilter, searchField, searchMethod, memoryTab, viewMode, setSearchParams]); + const deferUserParamUpdate = !!userParam && userParamValid && !userParamAppliedRef.current; + const userIdParam = deferUserParamUpdate ? userParam : selectedUser?.id || userParam; + if (userIdParam) { + params.set('user', userIdParam); + } + if (params.toString() !== searchParamsString) { + setSearchParams(params, { replace: true }); + } + }, [ + query, + memoryTypeFilter, + searchField, + searchMethod, + memoryTab, + viewMode, + selectedUser?.id, + userParam, + searchParamsString, + setSearchParams, + ]); const renderTabContent = () => { if (!selectedUser) return null; @@ -537,13 +592,13 @@ export const Memories: React.FC = () => {
); } - case 'knowledge_vault': { - const bucket = memoryCollections.knowledge_vault; - if (!bucket || bucket.items.length === 0) return emptyState('Knowledge Vault'); + case 'knowledge': { + const bucket = memoryCollections.knowledge; + if (!bucket || bucket.items.length === 0) return emptyState('Knowledge'); return (
- Showing {bucket.items.length} of {bucket.total_count} knowledge vault items + Showing {bucket.items.length} of {bucket.total_count} knowledge items
{bucket.items.map((item) => ( @@ -676,7 +731,7 @@ export const Memories: React.FC = () => { - +