mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 06:10:46 +00:00
7a4a20b2e8
* 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
72 lines
2.7 KiB
Go
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)
|
|
}
|
|
}
|