diff --git a/internal/store/pg/memory_docs.go b/internal/store/pg/memory_docs.go index 7394376b..bdc6b773 100644 --- a/internal/store/pg/memory_docs.go +++ b/internal/store/pg/memory_docs.go @@ -51,7 +51,16 @@ func (s *PGMemoryStore) GetDocument(ctx context.Context, agentID, userID, path s var content string var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + // Shared: no user_id filter + tc, tcArgs, _, tcErr := scopeClause(ctx, 3) + if tcErr != nil { + return "", tcErr + } + err = s.db.QueryRowContext(ctx, + "SELECT content FROM memory_documents WHERE agent_id = $1 AND path = $2"+tc+" ORDER BY updated_at DESC LIMIT 1", + append([]any{aid, path}, tcArgs...)...).Scan(&content) + } else if userID == "" { tc, tcArgs, _, tcErr := scopeClause(ctx, 3) if tcErr != nil { return "", tcErr @@ -100,7 +109,16 @@ func (s *PGMemoryStore) DeleteDocument(ctx context.Context, agentID, userID, pat aid := mustParseUUID(agentID) var res sql.Result var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + // Shared: delete any matching doc regardless of user_id + tc, tcArgs, _, tcErr := scopeClause(ctx, 3) + if tcErr != nil { + return tcErr + } + res, err = s.db.ExecContext(ctx, + "DELETE FROM memory_documents WHERE agent_id = $1 AND path = $2"+tc, + append([]any{aid, path}, tcArgs...)...) + } else if userID == "" { tc, tcArgs, _, tcErr := scopeClause(ctx, 3) if tcErr != nil { return tcErr @@ -132,7 +150,16 @@ func (s *PGMemoryStore) ListDocuments(ctx context.Context, agentID, userID strin var rows *sql.Rows var err error - if userID == "" { + if store.IsSharedMemory(ctx) { + // Shared: list ALL docs for agent (global + per-user from all users) + tc, tcArgs, _, tcErr := scopeClause(ctx, 2) + 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 = $1"+tc, + append([]any{aid}, tcArgs...)...) + } else if userID == "" { tc, tcArgs, _, tcErr := scopeClause(ctx, 2) if tcErr != nil { return nil, tcErr @@ -187,7 +214,16 @@ func (s *PGMemoryStore) IndexDocument(ctx context.Context, agentID, userID, path // Get document ID var docID uuid.UUID - if userID == "" { + if store.IsSharedMemory(ctx) { + // Shared: no user_id filter + tc, tcArgs, _, tcErr := scopeClause(ctx, 3) + if tcErr != nil { + return tcErr + } + err = s.db.QueryRowContext(ctx, + "SELECT id FROM memory_documents WHERE agent_id = $1 AND path = $2"+tc+" ORDER BY updated_at DESC LIMIT 1", + append([]any{aid, path}, tcArgs...)...).Scan(&docID) + } else if userID == "" { tc, tcArgs, _, tcErr := scopeClause(ctx, 3) if tcErr != nil { return tcErr diff --git a/internal/store/pg/memory_search.go b/internal/store/pg/memory_search.go index 80abfc89..add38dbc 100644 --- a/internal/store/pg/memory_search.go +++ b/internal/store/pg/memory_search.go @@ -81,9 +81,22 @@ func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID any var q string var args []any - if userID != "" { + if store.IsSharedMemory(ctx) { + // Shared: no user_id filter — search ALL chunks for agent + tc, tcArgs, _, err := scopeClause(ctx, 4) + if err != nil { + return nil, err + } + limitN := 4 + len(tcArgs) + q = fmt.Sprintf(`SELECT path, start_line, end_line, text, user_id, + ts_rank(tsv, plainto_tsquery('simple', $1)) AS score + FROM memory_chunks + WHERE agent_id = $2 AND tsv @@ plainto_tsquery('simple', $3)%s + ORDER BY score DESC LIMIT $%d`, tc, limitN) + args = append([]any{query, agentID, query}, tcArgs...) + args = append(args, limit) + } else if userID != "" { // fixed params: $1=query, $2=agentID, $3=query, $4=userID - // tenant clause appended at $5 (if filtered), then LIMIT at $5 or $6 tc, tcArgs, _, err := scopeClause(ctx, 5) if err != nil { return nil, err @@ -99,7 +112,6 @@ func (s *PGMemoryStore) ftsSearch(ctx context.Context, query string, agentID any args = append(args, limit) } else { // fixed params: $1=query, $2=agentID, $3=query - // tenant clause at $4 (if filtered), then LIMIT at $4 or $5 tc, tcArgs, _, err := scopeClause(ctx, 4) if err != nil { return nil, err @@ -136,9 +148,23 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a var q string var args []any - if userID != "" { + if store.IsSharedMemory(ctx) { + // Shared: no user_id filter — search ALL chunks for agent + tc, tcArgs, _, err := scopeClause(ctx, 3) + if err != nil { + return nil, err + } + orderN := 3 + len(tcArgs) + limitN := orderN + 1 + q = fmt.Sprintf(`SELECT path, start_line, end_line, text, user_id, + 1 - (embedding <=> $1::vector) AS score + FROM memory_chunks + WHERE agent_id = $2 AND embedding IS NOT NULL%s + ORDER BY embedding <=> $%d::vector LIMIT $%d`, tc, orderN, limitN) + args = append([]any{vecStr, agentID}, tcArgs...) + args = append(args, vecStr, limit) + } else if userID != "" { // fixed params: $1=vec, $2=agentID, $3=userID - // tenant clause at $4, then ORDER vec at $4+len(tcArgs), LIMIT after tc, tcArgs, _, err := scopeClause(ctx, 4) if err != nil { return nil, err @@ -155,7 +181,6 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a args = append(args, vecStr, limit) } else { // fixed params: $1=vec, $2=agentID - // tenant clause at $3, then ORDER vec at $3+len(tcArgs), LIMIT after tc, tcArgs, _, err := scopeClause(ctx, 3) if err != nil { return nil, err @@ -189,6 +214,8 @@ func (s *PGMemoryStore) vectorSearch(ctx context.Context, embedding []float32, a // hybridMerge combines FTS and vector results with weighted scoring. // Per-user results get a 1.2x boost. Deduplication: user copy wins over global. +// NOTE: when shared memory is active, the 1.2x personal boost still applies — +// consider removing it in shared mode if all docs should be treated equally. func hybridMerge(fts, vec []scoredChunk, textWeight, vectorWeight float64, currentUserID string) []store.MemorySearchResult { type key struct { Path string