Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion cmd/mnemonic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 27 additions & 6 deletions internal/api/routes/feedback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
12 changes: 1 addition & 11 deletions internal/api/routes/memories.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
45 changes: 43 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
},
Expand Down
73 changes: 53 additions & 20 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions internal/mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down