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()