diff --git a/internal/agent/loop.go b/internal/agent/loop.go index f6ba0bc4..4920c909 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -104,11 +104,10 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) cachedWs = ws } effectiveWorkspace := cachedWs.(string) - shared := l.shouldShareWorkspace(req.UserID, req.PeerKind) - if !shared { + if !l.shouldShareWorkspace(req.UserID, req.PeerKind) { effectiveWorkspace = filepath.Join(effectiveWorkspace, sanitizePathSegment(req.UserID)) - } else { - // Shared workspace → share memory across users too. + } + if l.shouldShareMemory() { ctx = store.WithSharedMemory(ctx) } if err := os.MkdirAll(effectiveWorkspace, 0755); err != nil { diff --git a/internal/agent/loop_utils.go b/internal/agent/loop_utils.go index 3663418f..e2008f5a 100644 --- a/internal/agent/loop_utils.go +++ b/internal/agent/loop_utils.go @@ -60,6 +60,12 @@ func (l *Loop) shouldShareWorkspace(userID, peerKind string) bool { return false } +// shouldShareMemory returns true if memory/KG should be shared across all users. +// Independent of workspace folder sharing. +func (l *Loop) shouldShareMemory() bool { + return l.workspaceSharing != nil && l.workspaceSharing.ShareMemory +} + // InvalidateUserWorkspace clears the cached workspace for a user, // forcing the next request to re-read from user_agent_profiles. func (l *Loop) InvalidateUserWorkspace(userID string) { diff --git a/internal/agent/workspace_sharing_test.go b/internal/agent/workspace_sharing_test.go index 59ec9883..f67bc82b 100644 --- a/internal/agent/workspace_sharing_test.go +++ b/internal/agent/workspace_sharing_test.go @@ -78,6 +78,49 @@ func TestShouldShareWorkspace_UnknownPeerKind(t *testing.T) { } } +// shouldShareMemory tests — independent of workspace sharing + +func TestShouldShareMemory_NilConfig(t *testing.T) { + l := &Loop{workspaceSharing: nil} + if l.shouldShareMemory() { + t.Error("nil config should not share memory") + } +} + +func TestShouldShareMemory_Enabled(t *testing.T) { + l := &Loop{workspaceSharing: &store.WorkspaceSharingConfig{ShareMemory: true}} + if !l.shouldShareMemory() { + t.Error("share_memory=true should share memory") + } +} + +func TestShouldShareMemory_DisabledByDefault(t *testing.T) { + l := &Loop{workspaceSharing: &store.WorkspaceSharingConfig{SharedDM: true}} + if l.shouldShareMemory() { + t.Error("SharedDM without ShareMemory should not share memory") + } +} + +func TestShouldShareMemory_IndependentOfWorkspace(t *testing.T) { + // share_memory=true but no workspace sharing → memory shared, workspace per-user + l := &Loop{workspaceSharing: &store.WorkspaceSharingConfig{ShareMemory: true}} + if !l.shouldShareMemory() { + t.Error("share_memory should work without workspace sharing") + } + if l.shouldShareWorkspace("user1", "direct") { + t.Error("workspace should NOT be shared when only share_memory is set") + } + + // workspace shared but share_memory=false → workspace shared, memory per-user + l2 := &Loop{workspaceSharing: &store.WorkspaceSharingConfig{SharedDM: true, ShareMemory: false}} + if l2.shouldShareMemory() { + t.Error("memory should NOT be shared when share_memory=false") + } + if !l2.shouldShareWorkspace("user1", "direct") { + t.Error("workspace should be shared when SharedDM=true") + } +} + func TestShouldShareWorkspace_BothEnabled(t *testing.T) { l := &Loop{workspaceSharing: &store.WorkspaceSharingConfig{ SharedDM: true, diff --git a/internal/store/agent_store.go b/internal/store/agent_store.go index 17fed3a0..60827c7c 100644 --- a/internal/store/agent_store.go +++ b/internal/store/agent_store.go @@ -161,6 +161,7 @@ type WorkspaceSharingConfig struct { SharedDM bool `json:"shared_dm"` SharedGroup bool `json:"shared_group"` SharedUsers []string `json:"shared_users,omitempty"` + ShareMemory bool `json:"share_memory"` } // ParseWorkspaceSharing extracts workspace_sharing from other_config JSONB. @@ -175,7 +176,7 @@ func (a *AgentData) ParseWorkspaceSharing() *WorkspaceSharingConfig { if json.Unmarshal(a.OtherConfig, &cfg) != nil || cfg.WS == nil { return nil } - if !cfg.WS.SharedDM && !cfg.WS.SharedGroup && len(cfg.WS.SharedUsers) == 0 { + if !cfg.WS.SharedDM && !cfg.WS.SharedGroup && len(cfg.WS.SharedUsers) == 0 && !cfg.WS.ShareMemory { return nil } return cfg.WS diff --git a/internal/store/pg/knowledge_graph_traversal.go b/internal/store/pg/knowledge_graph_traversal.go index d80bf75c..7445edd0 100644 --- a/internal/store/pg/knowledge_graph_traversal.go +++ b/internal/store/pg/knowledge_graph_traversal.go @@ -54,8 +54,8 @@ func (s *PGKnowledgeGraphStore) Traverse(ctx context.Context, agentID, userID, s p.path || e.id::text, r.relation_type FROM paths p - JOIN kg_relations r ON p.id = r.source_entity_id - JOIN kg_entities e ON r.target_entity_id = e.id + JOIN kg_relations r ON p.id = r.source_entity_id AND r.user_id = $3 + JOIN kg_entities e ON r.target_entity_id = e.id AND e.user_id = $3 WHERE p.depth < $4 AND NOT e.id::text = ANY(p.path) ) diff --git a/ui/web/src/i18n/locales/en/agents.json b/ui/web/src/i18n/locales/en/agents.json index 188365be..99fe52de 100644 --- a/ui/web/src/i18n/locales/en/agents.json +++ b/ui/web/src/i18n/locales/en/agents.json @@ -286,7 +286,12 @@ "sharedUsers": "Shared Users", "sharedUsersTip": "Specific user IDs that always share workspace regardless of DM/Group settings. Format: channel:userId (e.g. telegram:386246614)", "userIdPlaceholder": "Enter user ID (e.g. telegram:386246614)", - "warning": "When sharing is enabled, all shared users can read and write files in the same workspace directory. Memory is also shared; context files remain isolated per user. Toggling this setting does not migrate existing data — previous per-user memories will no longer be accessible." + "warning": "When sharing is enabled, all shared users can read and write files in the same workspace directory. Memory sharing is controlled separately. Context files remain isolated per user.", + "memoryGroupLabel": "Memory & Knowledge Graph", + "folderGroupLabel": "Workspace Folders", + "shareMemory": "Shared Memory & KG", + "shareMemoryTip": "All users access the same memory and knowledge graph. Independent of workspace folder sharing. When off, each user has their own isolated memory space.", + "shareMemoryNote": "Toggling this does not migrate data — existing per-user memories become inaccessible in shared mode." }, "compaction": { "title": "Compaction", diff --git a/ui/web/src/i18n/locales/vi/agents.json b/ui/web/src/i18n/locales/vi/agents.json index 2b23dac2..14539021 100644 --- a/ui/web/src/i18n/locales/vi/agents.json +++ b/ui/web/src/i18n/locales/vi/agents.json @@ -286,7 +286,12 @@ "sharedUsers": "User được chia sẻ", "sharedUsersTip": "Danh sách user ID cụ thể luôn dùng chung workspace. Định dạng: channel:userId (vd: telegram:386246614)", "userIdPlaceholder": "Nhập user ID (vd: telegram:386246614)", - "warning": "Khi bật chia sẻ, tất cả người dùng được chia sẻ có thể đọc và ghi file trong cùng thư mục workspace. Bộ nhớ cũng được chia sẻ; file ngữ cảnh vẫn được cô lập theo từng người dùng. Thay đổi cài đặt này không di chuyển dữ liệu cũ — bộ nhớ per-user trước đó sẽ không còn truy cập được." + "warning": "Khi bật chia sẻ, tất cả người dùng được chia sẻ có thể đọc và ghi file trong cùng thư mục workspace. Chia sẻ bộ nhớ được cài đặt riêng. File ngữ cảnh vẫn được cô lập theo từng người dùng.", + "memoryGroupLabel": "Memory & Knowledge Graph", + "folderGroupLabel": "Thư mục Workspace", + "shareMemory": "Chia sẻ Memory & KG", + "shareMemoryTip": "Tất cả người dùng truy cập chung Memory và Knowledge Graph. Độc lập với việc chia sẻ thư mục workspace. Khi tắt, mỗi người dùng có vùng nhớ riêng.", + "shareMemoryNote": "Bật/tắt không di chuyển dữ liệu — memory per-user trước đó sẽ không truy cập được trong chế độ shared." }, "compaction": { "title": "Nén ngữ cảnh", diff --git a/ui/web/src/i18n/locales/zh/agents.json b/ui/web/src/i18n/locales/zh/agents.json index e77331d3..81baaff3 100644 --- a/ui/web/src/i18n/locales/zh/agents.json +++ b/ui/web/src/i18n/locales/zh/agents.json @@ -286,7 +286,12 @@ "sharedUsers": "共享用户", "sharedUsersTip": "始终共享工作区的特定用户ID。格式:channel:userId(例如 telegram:386246614)", "userIdPlaceholder": "输入用户ID(例如 telegram:386246614)", - "warning": "启用共享后,所有共享用户可以读写同一工作区目录中的文件。记忆也会共享;上下文文件仍按用户隔离。切换此设置不会迁移现有数据——之前的用户独立记忆将无法访问。" + "warning": "启用共享后,所有共享用户可以读写同一工作区目录中的文件。记忆共享单独控制。上下文文件仍按用户隔离。", + "memoryGroupLabel": "记忆与知识图谱", + "folderGroupLabel": "工作区文件夹", + "shareMemory": "共享记忆与知识图谱", + "shareMemoryTip": "所有用户访问相同的记忆和知识图谱。与工作区文件夹共享无关。关闭时,每个用户拥有独立的记忆空间。", + "shareMemoryNote": "切换不会迁移数据——切换到共享模式后,现有的用户独立记忆将无法访问。" }, "compaction": { "title": "压缩", diff --git a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx index 953609b3..304e5952 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx @@ -100,7 +100,7 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) { if (thinkingLevel && thinkingLevel !== "off") { otherBase.thinking_level = thinkingLevel; } - if (wsSharing.shared_dm || wsSharing.shared_group || (wsSharing.shared_users?.length ?? 0) > 0) { + if (wsSharing.shared_dm || wsSharing.shared_group || (wsSharing.shared_users?.length ?? 0) > 0 || wsSharing.share_memory) { otherBase.workspace_sharing = wsSharing; } updates.other_config = Object.keys(otherBase).length > 0 ? otherBase : {}; diff --git a/ui/web/src/pages/agents/agent-detail/config-sections/workspace-sharing-section.tsx b/ui/web/src/pages/agents/agent-detail/config-sections/workspace-sharing-section.tsx index e74bb58b..34111699 100644 --- a/ui/web/src/pages/agents/agent-detail/config-sections/workspace-sharing-section.tsx +++ b/ui/web/src/pages/agents/agent-detail/config-sections/workspace-sharing-section.tsx @@ -4,7 +4,7 @@ import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Combobox } from "@/components/ui/combobox"; -import { Shield, X, AlertTriangle, Plus } from "lucide-react"; +import { Shield, X, AlertTriangle, Plus, Brain } from "lucide-react"; import { Button } from "@/components/ui/button"; import type { WorkspaceSharingConfig } from "@/types/agent"; import { useContacts } from "@/pages/contacts/hooks/use-contacts"; @@ -57,7 +57,7 @@ export function WorkspaceSharingSection({ value, onChange }: WorkspaceSharingSec return (
- {/* Header — prominent with shield icon */} + {/* Header */}
@@ -68,8 +68,8 @@ export function WorkspaceSharingSection({ value, onChange }: WorkspaceSharingSec
- {/* Content card with accent border when active */} -
+ {/* Content card */} +
{/* Security warning */} @@ -78,21 +78,44 @@ export function WorkspaceSharingSection({ value, onChange }: WorkspaceSharingSec - {/* DM / Group sharing toggles */} -
-
- {t(`${s}.sharedDm`)} + {/* Memory & Knowledge Graph — independent section */} +
+
+ + {t(`${s}.memoryGroupLabel`)} +
+
+ {t(`${s}.shareMemory`)} onChange({ ...value, shared_dm: v })} + checked={value.share_memory ?? false} + onCheckedChange={(v) => onChange({ ...value, share_memory: v })} />
-
- {t(`${s}.sharedGroup`)} - onChange({ ...value, shared_group: v })} - /> + {value.share_memory && ( +

+ {t(`${s}.shareMemoryNote`)} +

+ )} +
+ + {/* Workspace Folders — DM / Group sharing toggles */} +
+

{t(`${s}.folderGroupLabel`)}

+
+
+ {t(`${s}.sharedDm`)} + onChange({ ...value, shared_dm: v })} + /> +
+
+ {t(`${s}.sharedGroup`)} + onChange({ ...value, shared_group: v })} + /> +
diff --git a/ui/web/src/types/agent.ts b/ui/web/src/types/agent.ts index 3742a39e..36af00eb 100644 --- a/ui/web/src/types/agent.ts +++ b/ui/web/src/types/agent.ts @@ -81,6 +81,7 @@ export interface WorkspaceSharingConfig { shared_dm?: boolean; shared_group?: boolean; shared_users?: string[]; + share_memory?: boolean; } export interface AgentData {