From 2fa715ba4e4648c4f6a20c50d3e31acf6c060f83 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Sun, 17 May 2026 15:33:27 +0700 Subject: [PATCH] fix(context): share agent scoped context data --- internal/agent/loop_context.go | 4 +++ internal/store/context.go | 28 +++++++++++++++ internal/store/run_context.go | 3 +- internal/store/sqlitestore/memory_docs.go | 40 ++++++++++++++++++--- internal/store/sqlitestore/memory_search.go | 15 +++++++- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/internal/agent/loop_context.go b/internal/agent/loop_context.go index 7767ecab..d7b69af3 100644 --- a/internal/agent/loop_context.go +++ b/internal/agent/loop_context.go @@ -156,6 +156,9 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup } // Apply user isolation layer via pipeline. shared := l.shouldShareWorkspace(req.UserID, req.PeerKind) + if shared { + ctx = store.WithSharedContext(ctx) + } effectiveWorkspace := tools.ResolveWorkspace(ws, tools.UserChatLayer(tools.SanitizePathSegment(req.UserID), shared), ) @@ -359,6 +362,7 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup SharedMemory: store.IsSharedMemory(ctx), SharedKG: store.IsSharedKG(ctx), SharedSessions: store.IsSharedSessions(ctx), + SharedContext: store.IsSharedContext(ctx), RestrictToWorkspace: l.restrictToWs != nil && *l.restrictToWs, BuiltinToolSettings: l.builtinToolSettings, ChannelType: req.ChannelType, diff --git a/internal/store/context.go b/internal/store/context.go index 18ee3ebf..fcc6199b 100644 --- a/internal/store/context.go +++ b/internal/store/context.go @@ -31,6 +31,8 @@ const ( SharedKGKey contextKey = "goclaw_shared_kg" // SharedSessionsKey indicates sessions should be shared across all users (no per-group scoping). SharedSessionsKey contextKey = "goclaw_shared_sessions" + // SharedContextKey indicates context files should be read/written at agent scope. + SharedContextKey contextKey = "goclaw_shared_context" // ShellDenyGroupsKey holds per-agent shell deny group overrides. ShellDenyGroupsKey contextKey = "goclaw_shell_deny_groups" // AgentKeyKey is the context key for the agent key/name (string identifier, e.g. "default"). @@ -282,6 +284,32 @@ func MemoryUserID(ctx context.Context) string { return UserIDFromContext(ctx) } +// WithSharedContext returns a context flagged for shared context files. +func WithSharedContext(ctx context.Context) context.Context { + return context.WithValue(ctx, SharedContextKey, true) +} + +// IsSharedContext returns true if context files should use agent-level scope. +func IsSharedContext(ctx context.Context) bool { + if v, ok := ctx.Value(SharedContextKey).(bool); ok { + return v + } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.SharedContext + } + return false +} + +// ContextUserID returns the userID to use for context-file operations. +// Shared workspace mode maps virtual context files to the agent-level store so +// read_file/write_file behavior matches the visible shared workspace path. +func ContextUserID(ctx context.Context) string { + if IsSharedContext(ctx) { + return "" + } + return UserIDFromContext(ctx) +} + // KGUserID returns the userID to use for knowledge graph operations. // Returns "" (agent-level scope) when shared KG is active, otherwise the per-user ID. func KGUserID(ctx context.Context) string { diff --git a/internal/store/run_context.go b/internal/store/run_context.go index 35c51faf..98674803 100644 --- a/internal/store/run_context.go +++ b/internal/store/run_context.go @@ -34,6 +34,7 @@ type RunContext struct { SharedMemory bool SharedKG bool SharedSessions bool + SharedContext bool RestrictToWorkspace bool // Tool configuration @@ -52,7 +53,7 @@ type RunContext struct { TeamID string WorkspaceChannel string WorkspaceChatID string - TeamIsolated bool // true when team.workspace_scope != "shared" — drives chat_id filtering in vault search + TeamIsolated bool // true when team.workspace_scope != "shared" — drives chat_id filtering in vault search TeamTaskID string DelegationID string // delegation identifier for vault auto-linking (empty when not in delegation) LeaderAgentID string // leader's agent UUID for member memory read fallback diff --git a/internal/store/sqlitestore/memory_docs.go b/internal/store/sqlitestore/memory_docs.go index fcbae213..8f83037d 100644 --- a/internal/store/sqlitestore/memory_docs.go +++ b/internal/store/sqlitestore/memory_docs.go @@ -20,7 +20,15 @@ func (s *SQLiteMemoryStore) GetDocument(ctx context.Context, agentID, userID, pa var content string var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + tc, tcArgs, tcErr := scopeClause(ctx) + if tcErr != nil { + return "", tcErr + } + err = s.db.QueryRowContext(ctx, + "SELECT content FROM memory_documents WHERE agent_id = ? AND path = ?"+tc+" ORDER BY updated_at DESC LIMIT 1", + append([]any{aid, path}, tcArgs...)...).Scan(&content) + } else if userID == "" { tc, tcArgs, tcErr := scopeClause(ctx) if tcErr != nil { return "", tcErr @@ -69,7 +77,15 @@ func (s *SQLiteMemoryStore) DeleteDocument(ctx context.Context, agentID, userID, var res sql.Result var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + tc, tcArgs, tcErr := scopeClause(ctx) + if tcErr != nil { + return tcErr + } + res, err = s.db.ExecContext(ctx, + "DELETE FROM memory_documents WHERE agent_id = ? AND path = ?"+tc, + append([]any{agentID, path}, tcArgs...)...) + } else if userID == "" { tc, tcArgs, tcErr := scopeClause(ctx) if tcErr != nil { return tcErr @@ -100,7 +116,15 @@ func (s *SQLiteMemoryStore) ListDocuments(ctx context.Context, agentID, userID s var rows *sql.Rows var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + tc, tcArgs, tcErr := scopeClause(ctx) + if tcErr != nil { + return nil, tcErr + } + rows, err = s.db.QueryContext(ctx, + "SELECT path, hash, user_id, updated_at FROM memory_documents WHERE agent_id = ?"+tc, + append([]any{agentID}, tcArgs...)...) + } else if userID == "" { tc, tcArgs, tcErr := scopeClause(ctx) if tcErr != nil { return nil, tcErr @@ -147,7 +171,15 @@ func (s *SQLiteMemoryStore) IndexDocument(ctx context.Context, agentID, userID, // Get document ID var docID string - if userID == "" { + if store.IsSharedMemory(ctx) { + tc, tcArgs, tcErr := scopeClause(ctx) + if tcErr != nil { + return tcErr + } + err = s.db.QueryRowContext(ctx, + "SELECT id FROM memory_documents WHERE agent_id = ? AND path = ?"+tc+" ORDER BY updated_at DESC LIMIT 1", + append([]any{agentID, path}, tcArgs...)...).Scan(&docID) + } else if userID == "" { tc, tcArgs, tcErr := scopeClause(ctx) if tcErr != nil { return tcErr diff --git a/internal/store/sqlitestore/memory_search.go b/internal/store/sqlitestore/memory_search.go index 5ab22660..490704f7 100644 --- a/internal/store/sqlitestore/memory_search.go +++ b/internal/store/sqlitestore/memory_search.go @@ -48,7 +48,20 @@ func (s *SQLiteMemoryStore) likeSearch(ctx context.Context, query, agentID, user var q string var args []any - if userID != "" { + if store.IsSharedMemory(ctx) { + tc, tcArgs, err := scopeClause(ctx) + if err != nil { + return nil, err + } + q = `SELECT path, start_line, end_line, text, user_id + FROM memory_chunks + WHERE agent_id = ? + AND text LIKE ? ESCAPE '\'` + tc + ` + ORDER BY updated_at DESC + LIMIT ?` + args = append([]any{agentID, pattern}, tcArgs...) + args = append(args, limit) + } else if userID != "" { tc, tcArgs, err := scopeClause(ctx) if err != nil { return nil, err