From d66b349e30c881b0510910a89a0c5b1d57ab1ab9 Mon Sep 17 00:00:00 2001 From: Caleb Gross Date: Sat, 21 Mar 2026 11:59:05 -0400 Subject: [PATCH] feat: make MCP/API salience and feedback weights configurable Add memory_defaults config section with initial salience per memory type and feedback tuning weights. Eliminates duplicate switch statements between MCP server and API routes. MCP server receives MemoryDefaults via constructor. API routes use exported package-level vars set at startup from config. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mnemonic/main.go | 20 ++++++++- internal/api/routes/feedback.go | 33 ++++++++++++--- internal/api/routes/memories.go | 12 +----- internal/config/config.go | 45 +++++++++++++++++++- internal/mcp/server.go | 73 ++++++++++++++++++++++++--------- internal/mcp/server_test.go | 8 ++-- 6 files changed, 147 insertions(+), 44 deletions(-) diff --git a/cmd/mnemonic/main.go b/cmd/mnemonic/main.go index bad38838..9f4f96a8 100644 --- a/cmd/mnemonic/main.go +++ b/cmd/mnemonic/main.go @@ -39,6 +39,7 @@ import ( "github.com/appsprout-dev/mnemonic/internal/agent/reactor" "github.com/appsprout-dev/mnemonic/internal/agent/retrieval" "github.com/appsprout-dev/mnemonic/internal/api" + "github.com/appsprout-dev/mnemonic/internal/api/routes" "github.com/appsprout-dev/mnemonic/internal/backup" "github.com/appsprout-dev/mnemonic/internal/mcp" "github.com/appsprout-dev/mnemonic/internal/store" @@ -1713,6 +1714,13 @@ func serveCommand(configPath string) { apiDeps.AgentWebPort = cfg.AgentSDK.WebPort } + // Set API routes memory defaults from config + routes.FeedbackStrengthDelta = cfg.MemoryDefaults.FeedbackStrengthDelta + routes.FeedbackSalienceBoost = cfg.MemoryDefaults.FeedbackSalienceBoost + routes.InitialSalienceForType = func(memType string) float32 { + return cfg.MemoryDefaults.SalienceForType(memType) + } + apiServer := api.NewServer(api.ServerConfig{ Host: cfg.API.Host, Port: cfg.API.Port, @@ -2668,7 +2676,17 @@ func mcpCommand(configPath string) { mcpResolver := config.NewProjectResolver(cfg.Projects) daemonURL := fmt.Sprintf("http://%s:%d", cfg.API.Host, cfg.API.Port) - server := mcp.NewMCPServer(db, retriever, bus, log, Version, cfg.Coaching.CoachingFile, cfg.Perception.Filesystem.ExcludePatterns, cfg.Perception.Filesystem.MaxContentBytes, mcpResolver, daemonURL) + memDefaults := mcp.MemoryDefaults{ + SalienceGeneral: cfg.MemoryDefaults.InitialSalienceGeneral, + SalienceDecision: cfg.MemoryDefaults.InitialSalienceDecision, + SalienceError: cfg.MemoryDefaults.InitialSalienceError, + SalienceInsight: cfg.MemoryDefaults.InitialSalienceInsight, + SalienceLearning: cfg.MemoryDefaults.InitialSalienceLearning, + SalienceHandoff: cfg.MemoryDefaults.InitialSalienceHandoff, + FeedbackStrengthDelta: cfg.MemoryDefaults.FeedbackStrengthDelta, + FeedbackSalienceBoost: cfg.MemoryDefaults.FeedbackSalienceBoost, + } + server := mcp.NewMCPServer(db, retriever, bus, log, Version, cfg.Coaching.CoachingFile, cfg.Perception.Filesystem.ExcludePatterns, cfg.Perception.Filesystem.MaxContentBytes, mcpResolver, daemonURL, memDefaults) // Handle signal for graceful shutdown sigChan := make(chan os.Signal, 1) diff --git a/internal/api/routes/feedback.go b/internal/api/routes/feedback.go index b8243918..47e05fa6 100644 --- a/internal/api/routes/feedback.go +++ b/internal/api/routes/feedback.go @@ -11,11 +11,32 @@ import ( "github.com/google/uuid" ) -const ( - feedbackStrengthDelta float32 = 0.05 - feedbackSalienceBoost float32 = 0.02 +// FeedbackStrengthDelta and FeedbackSalienceBoost are tunable via MemoryDefaults. +// Package-level defaults; override via SetMemoryDefaults before registering routes. +var ( + FeedbackStrengthDelta float32 = 0.05 + FeedbackSalienceBoost float32 = 0.02 ) +// InitialSalienceForType returns the configured initial salience for a memory type. +// Package-level defaults; override via SetMemoryDefaults before registering routes. +var InitialSalienceForType = defaultSalienceForType + +func defaultSalienceForType(memType string) float32 { + switch memType { + case "decision": + return 0.85 + case "error": + return 0.8 + case "insight": + return 0.9 + case "learning": + return 0.8 + default: + return 0.7 + } +} + // FeedbackRequest is the JSON request body for submitting recall feedback. type FeedbackRequest struct { QueryID string `json:"query_id"` @@ -97,7 +118,7 @@ func HandleFeedback(s store.Store, log *slog.Logger) http.HandlerFunc { } for _, a := range assocs { if a.TargetID == ta.TargetID { - newStrength := a.Strength + feedbackStrengthDelta + newStrength := a.Strength + FeedbackStrengthDelta if newStrength > 1.0 { newStrength = 1.0 } @@ -114,7 +135,7 @@ func HandleFeedback(s store.Store, log *slog.Logger) http.HandlerFunc { if err != nil { continue } - newSalience := mem.Salience + feedbackSalienceBoost + newSalience := mem.Salience + FeedbackSalienceBoost if newSalience > 1.0 { newSalience = 1.0 } @@ -132,7 +153,7 @@ func HandleFeedback(s store.Store, log *slog.Logger) http.HandlerFunc { } for _, a := range assocs { if a.TargetID == ta.TargetID { - newStrength := a.Strength - feedbackStrengthDelta + newStrength := a.Strength - FeedbackStrengthDelta if newStrength < 0.05 { newStrength = 0.05 } diff --git a/internal/api/routes/memories.go b/internal/api/routes/memories.go index 8bfc3cb2..5c1ba4a4 100644 --- a/internal/api/routes/memories.go +++ b/internal/api/routes/memories.go @@ -89,17 +89,7 @@ func HandleCreateMemory(s store.Store, bus events.Bus, log *slog.Logger) http.Ha // Create RawMemory now := time.Now() - salience := float32(0.7) - switch req.Type { - case "decision": - salience = 0.85 - case "error": - salience = 0.8 - case "insight": - salience = 0.9 - case "learning": - salience = 0.8 - } + salience := InitialSalienceForType(req.Type) rawMem := store.RawMemory{ ID: uuid.New().String(), diff --git a/internal/config/config.go b/internal/config/config.go index c5308df5..60677dcd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,8 +28,9 @@ type Config struct { Episoding EpisodingConfig `yaml:"episoding"` Abstraction AbstractionConfig `yaml:"abstraction"` Orchestrator OrchestratorConfig `yaml:"orchestrator"` - Reactor ReactorConfig `yaml:"reactor"` - MCP MCPConfig `yaml:"mcp"` + Reactor ReactorConfig `yaml:"reactor"` + MemoryDefaults MemoryDefaultsConfig `yaml:"memory_defaults"` + MCP MCPConfig `yaml:"mcp"` AgentSDK AgentSDKConfig `yaml:"agent_sdk"` Training TrainingConfig `yaml:"training"` Coaching CoachingConfig `yaml:"coaching"` @@ -363,6 +364,36 @@ type ReactorConfig struct { Cooldowns map[string]string `yaml:"cooldowns"` // chain ID -> duration string (e.g., "30m", "1h") } +// MemoryDefaultsConfig holds shared defaults used by both MCP and API. +type MemoryDefaultsConfig struct { + InitialSalienceGeneral float32 `yaml:"initial_salience_general"` // default: 0.7 + InitialSalienceDecision float32 `yaml:"initial_salience_decision"` // default: 0.85 + InitialSalienceError float32 `yaml:"initial_salience_error"` // default: 0.8 + InitialSalienceInsight float32 `yaml:"initial_salience_insight"` // default: 0.9 + InitialSalienceLearning float32 `yaml:"initial_salience_learning"` // default: 0.8 + InitialSalienceHandoff float32 `yaml:"initial_salience_handoff"` // default: 0.95 + FeedbackStrengthDelta float32 `yaml:"feedback_strength_delta"` // default: 0.05 + FeedbackSalienceBoost float32 `yaml:"feedback_salience_boost"` // default: 0.02 +} + +// SalienceForType returns the initial salience for a given memory type. +func (c MemoryDefaultsConfig) SalienceForType(memType string) float32 { + switch memType { + case "decision": + return c.InitialSalienceDecision + case "error": + return c.InitialSalienceError + case "insight": + return c.InitialSalienceInsight + case "learning": + return c.InitialSalienceLearning + case "handoff": + return c.InitialSalienceHandoff + default: + return c.InitialSalienceGeneral + } +} + // MCPConfig holds MCP server settings. type MCPConfig struct { Enabled bool `yaml:"enabled"` @@ -735,6 +766,16 @@ func Default() *Config { HealthReportInterval: 5 * time.Minute, }, Reactor: ReactorConfig{}, + MemoryDefaults: MemoryDefaultsConfig{ + InitialSalienceGeneral: 0.7, + InitialSalienceDecision: 0.85, + InitialSalienceError: 0.8, + InitialSalienceInsight: 0.9, + InitialSalienceLearning: 0.8, + InitialSalienceHandoff: 0.95, + FeedbackStrengthDelta: 0.05, + FeedbackSalienceBoost: 0.02, + }, MCP: MCPConfig{ Enabled: true, }, diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 893949f8..7937c817 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -48,6 +48,50 @@ type ProjectResolver interface { Resolve(input string) string } +// MemoryDefaults holds shared salience and feedback tuning values. +type MemoryDefaults struct { + SalienceGeneral float32 + SalienceDecision float32 + SalienceError float32 + SalienceInsight float32 + SalienceLearning float32 + SalienceHandoff float32 + FeedbackStrengthDelta float32 + FeedbackSalienceBoost float32 +} + +// SalienceForType returns the initial salience for a given memory type. +func (d MemoryDefaults) SalienceForType(memType string) float32 { + switch memType { + case "decision": + return d.SalienceDecision + case "error": + return d.SalienceError + case "insight": + return d.SalienceInsight + case "learning": + return d.SalienceLearning + case "handoff": + return d.SalienceHandoff + default: + return d.SalienceGeneral + } +} + +// DefaultMemoryDefaults returns the built-in defaults (used when no config override). +func DefaultMemoryDefaults() MemoryDefaults { + return MemoryDefaults{ + SalienceGeneral: 0.7, + SalienceDecision: 0.85, + SalienceError: 0.8, + SalienceInsight: 0.9, + SalienceLearning: 0.8, + SalienceHandoff: 0.95, + FeedbackStrengthDelta: 0.05, + FeedbackSalienceBoost: 0.02, + } +} + // MCPServer implements the Model Context Protocol over JSON-RPC 2.0 type MCPServer struct { store store.Store @@ -61,6 +105,7 @@ type MCPServer struct { coachingFile string // path for coach_local_llm writes excludePatterns []string maxContentBytes int + memDefaults MemoryDefaults // shared salience and feedback tuning // Proactive context state (session-scoped) lastContextTime time.Time // watermark for get_context polling @@ -77,7 +122,7 @@ type MCPServer struct { } // NewMCPServer creates a new MCP server with the given dependencies. -func NewMCPServer(s store.Store, r *retrieval.RetrievalAgent, bus events.Bus, log *slog.Logger, version string, coachingFile string, excludePatterns []string, maxContentBytes int, resolver ProjectResolver, daemonURL string) *MCPServer { +func NewMCPServer(s store.Store, r *retrieval.RetrievalAgent, bus events.Bus, log *slog.Logger, version string, coachingFile string, excludePatterns []string, maxContentBytes int, resolver ProjectResolver, daemonURL string, memDefaults MemoryDefaults) *MCPServer { // Auto-detect project from working directory wd, _ := os.Getwd() var project string @@ -105,6 +150,7 @@ func NewMCPServer(s store.Store, r *retrieval.RetrievalAgent, bus events.Bus, lo coachingFile: coachingFile, excludePatterns: excludePatterns, maxContentBytes: maxContentBytes, + memDefaults: memDefaults, daemonURL: daemonURL, lastContextTime: time.Now(), sessionRecalledIDs: make(map[string]bool), @@ -455,16 +501,7 @@ func (srv *MCPServer) handleRemember(ctx context.Context, args map[string]interf } // Boost salience for specific types - switch memType { - case "decision": - raw.InitialSalience = 0.85 - case "error": - raw.InitialSalience = 0.8 - case "insight": - raw.InitialSalience = 0.9 - case "learning": - raw.InitialSalience = 0.8 - } + raw.InitialSalience = srv.memDefaults.SalienceForType(memType) if err := srv.store.WriteRaw(ctx, raw); err != nil { srv.log.Error("failed to write raw memory", "error", err) @@ -1823,11 +1860,7 @@ func (srv *MCPServer) handleGetInsights(ctx context.Context, args map[string]int return toolResult(text), nil } -// Feedback tuning constants -const ( - feedbackStrengthDelta float32 = 0.05 - feedbackSalienceBoost float32 = 0.02 -) +// srv.memDefaults.FeedbackStrengthDelta and srv.memDefaults.FeedbackSalienceBoost are now on srv.memDefaults. // handleFeedback records quality feedback for a recall result and adjusts association strengths. func (srv *MCPServer) handleFeedback(ctx context.Context, args map[string]interface{}) (interface{}, error) { @@ -1894,7 +1927,7 @@ func (srv *MCPServer) handleFeedback(ctx context.Context, args map[string]interf } for _, a := range assocs { if a.TargetID == ta.TargetID { - newStrength := a.Strength + feedbackStrengthDelta + newStrength := a.Strength + srv.memDefaults.FeedbackStrengthDelta if newStrength > 1.0 { newStrength = 1.0 } @@ -1911,7 +1944,7 @@ func (srv *MCPServer) handleFeedback(ctx context.Context, args map[string]interf if err != nil { continue } - newSalience := mem.Salience + feedbackSalienceBoost + newSalience := mem.Salience + srv.memDefaults.FeedbackSalienceBoost if newSalience > 1.0 { newSalience = 1.0 } @@ -1929,7 +1962,7 @@ func (srv *MCPServer) handleFeedback(ctx context.Context, args map[string]interf } for _, a := range assocs { if a.TargetID == ta.TargetID { - newStrength := a.Strength - feedbackStrengthDelta + newStrength := a.Strength - srv.memDefaults.FeedbackStrengthDelta if newStrength < 0.05 { newStrength = 0.05 } @@ -2671,7 +2704,7 @@ func (srv *MCPServer) handleCreateHandoff(ctx context.Context, args map[string]i Timestamp: time.Now(), CreatedAt: time.Now(), HeuristicScore: 0.9, - InitialSalience: 0.95, + InitialSalience: srv.memDefaults.SalienceForType("handoff"), Processed: false, Project: srv.project, SessionID: srv.sessionID, diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 06c0af1e..96644c29 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -30,7 +30,7 @@ func (m *mockBus) Close() error { return nil } // TestHandleInitialize tests handleInitialize returns correct protocol version and server info. func TestHandleInitialize(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "") + srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "", DefaultMemoryDefaults()) req := &jsonRPCRequest{ JSONRPC: "2.0", @@ -89,7 +89,7 @@ func TestHandleInitialize(t *testing.T) { // TestHandleToolsList tests handleToolsList returns all 10 tools. func TestHandleToolsList(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "") + srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "", DefaultMemoryDefaults()) req := &jsonRPCRequest{ JSONRPC: "2.0", @@ -299,7 +299,7 @@ func TestSuccessResponse(t *testing.T) { // TestHandleRequestDispatch tests that handleRequest correctly dispatches to handlers. func TestHandleRequestDispatch(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "") + srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "", DefaultMemoryDefaults()) tests := []struct { method string @@ -407,7 +407,7 @@ func TestFormatDuration(t *testing.T) { // TestCheckAcceptance tests that suggested IDs are detected in recall results. func TestCheckAcceptance(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "") + srv := NewMCPServer(&mockStore{}, nil, &mockBus{}, logger, "test", "", []string{}, 0, nil, "", DefaultMemoryDefaults()) // Simulate get_context suggesting two memory IDs. srv.contextSuggestedIDs["abc-123"] = time.Now()