diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 6ecf4b68..f6ba0bc4 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -104,8 +104,12 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) cachedWs = ws } effectiveWorkspace := cachedWs.(string) - if !l.shouldShareWorkspace(req.UserID, req.PeerKind) { + shared := l.shouldShareWorkspace(req.UserID, req.PeerKind) + if !shared { effectiveWorkspace = filepath.Join(effectiveWorkspace, sanitizePathSegment(req.UserID)) + } else { + // Shared workspace → share memory across users too. + ctx = store.WithSharedMemory(ctx) } if err := os.MkdirAll(effectiveWorkspace, 0755); err != nil { slog.Warn("failed to create user workspace directory", "workspace", effectiveWorkspace, "user", req.UserID, "error", err) diff --git a/internal/store/context.go b/internal/store/context.go index 4edfad5c..2f0461b4 100644 --- a/internal/store/context.go +++ b/internal/store/context.go @@ -23,6 +23,8 @@ const ( SelfEvolveKey contextKey = "goclaw_self_evolve" // LocaleKey is the context key for the user's preferred locale (e.g. "en", "vi", "zh"). LocaleKey contextKey = "goclaw_locale" + // SharedMemoryKey indicates memory should be shared (no per-user scoping). + SharedMemoryKey contextKey = "goclaw_shared_memory" ) // WithUserID returns a new context with the given user ID. @@ -90,6 +92,26 @@ func SelfEvolveFromContext(ctx context.Context) bool { return false } +// WithSharedMemory returns a context flagged for shared memory (skip per-user scoping). +func WithSharedMemory(ctx context.Context) context.Context { + return context.WithValue(ctx, SharedMemoryKey, true) +} + +// IsSharedMemory returns true if memory should be shared across users. +func IsSharedMemory(ctx context.Context) bool { + v, _ := ctx.Value(SharedMemoryKey).(bool) + return v +} + +// MemoryUserID returns the userID to use for memory operations. +// Returns "" (shared/global) when shared memory is active, otherwise the per-user ID. +func MemoryUserID(ctx context.Context) string { + if IsSharedMemory(ctx) { + return "" + } + return UserIDFromContext(ctx) +} + // WithLocale returns a new context with the given locale. func WithLocale(ctx context.Context, locale string) context.Context { return context.WithValue(ctx, LocaleKey, locale) diff --git a/internal/tools/knowledge_graph.go b/internal/tools/knowledge_graph.go index 5e81660e..ce12469b 100644 --- a/internal/tools/knowledge_graph.go +++ b/internal/tools/knowledge_graph.go @@ -65,7 +65,7 @@ func (t *KnowledgeGraphSearchTool) Execute(ctx context.Context, args map[string] if agentID == uuid.Nil { return ErrorResult("agent context not available") } - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) query, _ := args["query"].(string) if query == "" { diff --git a/internal/tools/memory.go b/internal/tools/memory.go index 86468362..c2dd43c5 100644 --- a/internal/tools/memory.go +++ b/internal/tools/memory.go @@ -72,7 +72,7 @@ func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]any) *Re return ErrorResult("memory system not available") } - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) searchOpts := store.MemorySearchOptions{ MaxResults: maxResults, MinScore: minScore, @@ -167,7 +167,7 @@ func (t *MemoryGetTool) Execute(ctx context.Context, args map[string]any) *Resul return ErrorResult("memory system not available") } - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) // Try per-user first, then global content, err := t.memStore.GetDocument(ctx, agentID.String(), userID, path) diff --git a/internal/tools/memory_interceptor.go b/internal/tools/memory_interceptor.go index 98dbee62..d7bfd2e0 100644 --- a/internal/tools/memory_interceptor.go +++ b/internal/tools/memory_interceptor.go @@ -96,7 +96,7 @@ func (m *MemoryInterceptor) ReadFile(ctx context.Context, path string) (string, // Normalize absolute path to workspace-relative for DB storage relPath := normalizeToRelative(path, m.workspace) - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) agentStr := agentID.String() // Try per-user first, then global @@ -136,7 +136,7 @@ func (m *MemoryInterceptor) WriteFile(ctx context.Context, path, content string) // Normalize absolute path to workspace-relative for DB storage relPath := normalizeToRelative(path, m.workspace) - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) agentStr := agentID.String() // Write document to DB @@ -175,7 +175,7 @@ func (m *MemoryInterceptor) ListFiles(ctx context.Context, path string) (string, return "", false, nil } - userID := store.UserIDFromContext(ctx) + userID := store.MemoryUserID(ctx) docs, err := m.memStore.ListDocuments(ctx, agentID.String(), userID) if err != nil { return "", true, err diff --git a/ui/web/src/i18n/locales/en/agents.json b/ui/web/src/i18n/locales/en/agents.json index 018f3c16..188365be 100644 --- a/ui/web/src/i18n/locales/en/agents.json +++ b/ui/web/src/i18n/locales/en/agents.json @@ -286,7 +286,7 @@ "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 and context files remain isolated per user." + "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." }, "compaction": { "title": "Compaction", diff --git a/ui/web/src/i18n/locales/vi/agents.json b/ui/web/src/i18n/locales/vi/agents.json index a36bf423..2b23dac2 100644 --- a/ui/web/src/i18n/locales/vi/agents.json +++ b/ui/web/src/i18n/locales/vi/agents.json @@ -286,7 +286,7 @@ "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ớ và file ngữ cảnh vẫn được cô lập theo từng người dùng." + "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." }, "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 68d110fc..e77331d3 100644 --- a/ui/web/src/i18n/locales/zh/agents.json +++ b/ui/web/src/i18n/locales/zh/agents.json @@ -286,7 +286,7 @@ "sharedUsers": "共享用户", "sharedUsersTip": "始终共享工作区的特定用户ID。格式:channel:userId(例如 telegram:386246614)", "userIdPlaceholder": "输入用户ID(例如 telegram:386246614)", - "warning": "启用共享后,所有共享用户可以读写同一工作区目录中的文件。记忆和上下文文件仍按用户隔离。" + "warning": "启用共享后,所有共享用户可以读写同一工作区目录中的文件。记忆也会共享;上下文文件仍按用户隔离。切换此设置不会迁移现有数据——之前的用户独立记忆将无法访问。" }, "compaction": { "title": "压缩",