Files
goclaw/cmd/gateway_callbacks.go
T
Goon 7a4a20b2e8 fix(discord): per-user memory scope in guild channels (#166)
* docs: add brainstorm report for discord guild-user memory

* docs: update brainstorm report with corrected root cause analysis

* feat(discord): per-user memory scope in guild channels

Fixes shared USER.md between guild members by scoping userID to
"guild:{guildID}:user:{senderID}" for Discord group messages.
Updates all group-context prefix checks (write permissions, writer
cache, cron peer kind, history filter) to include the new guild: prefix.

Closes #165
2026-03-12 16:45:30 +07:00

72 lines
2.7 KiB
Go

package cmd
import (
"context"
"log/slog"
"strings"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/agent"
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/tools"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// buildEnsureUserFiles creates the per-user file seeding callback.
// Seeds per-user context files on first chat (new user profile).
func buildEnsureUserFiles(as store.AgentStore, msgBus *bus.MessageBus) agent.EnsureUserFilesFunc {
return func(ctx context.Context, agentID uuid.UUID, userID, agentType, workspace, channel string) (string, error) {
isNew, effectiveWs, err := as.GetOrCreateUserProfile(ctx, agentID, userID, workspace, channel)
if err != nil {
return effectiveWs, err
}
if !isNew {
return effectiveWs, nil // already profiled = already seeded
}
// Auto-add first group member as a file writer (bootstrap the allowlist).
if strings.HasPrefix(userID, "group:") || strings.HasPrefix(userID, "guild:") {
senderID := store.SenderIDFromContext(ctx)
if senderID != "" {
parts := strings.SplitN(senderID, "|", 2)
numericID := parts[0]
senderUsername := ""
if len(parts) > 1 {
senderUsername = parts[1]
}
if addErr := as.AddGroupFileWriter(ctx, agentID, userID, numericID, "", senderUsername); addErr != nil {
slog.Warn("failed to auto-add group file writer", "error", addErr, "sender", numericID, "group", userID)
} else if msgBus != nil {
msgBus.Broadcast(bus.Event{
Name: protocol.EventCacheInvalidate,
Payload: bus.CacheInvalidatePayload{Kind: bus.CacheKindGroupFileWriters, Key: userID},
})
}
}
}
_, err = bootstrap.SeedUserFiles(ctx, as, agentID, userID, agentType)
return effectiveWs, err
}
}
// buildBootstrapCleanup creates a callback that removes BOOTSTRAP.md for a user.
// Used as a safety net after enough conversation turns, in case the LLM
// didn't clear BOOTSTRAP.md itself. Idempotent — no-op if already cleared.
func buildBootstrapCleanup(as store.AgentStore) agent.BootstrapCleanupFunc {
return func(ctx context.Context, agentID uuid.UUID, userID string) error {
return as.DeleteUserContextFile(ctx, agentID, userID, bootstrap.BootstrapFile)
}
}
// buildContextFileLoader creates the per-request context file loader callback.
// Delegates to the ContextFileInterceptor for type-aware routing.
func buildContextFileLoader(intc *tools.ContextFileInterceptor) agent.ContextFileLoaderFunc {
return func(ctx context.Context, agentID uuid.UUID, userID, agentType string) []bootstrap.ContextFile {
return intc.LoadContextFiles(ctx, agentID, userID, agentType)
}
}