diff --git a/cmd/mnemonic/main.go b/cmd/mnemonic/main.go index a9b0a69f..cee9f66a 100644 --- a/cmd/mnemonic/main.go +++ b/cmd/mnemonic/main.go @@ -1381,10 +1381,16 @@ func serveCommand(configPath string) { // --- Start episoding agent (groups raw events into episodes) --- var episodingAgent *episoding.EpisodingAgent if cfg.Episoding.Enabled { + pollingInterval := time.Duration(cfg.Episoding.PollingIntervalSec) * time.Second + if pollingInterval <= 0 { + pollingInterval = 10 * time.Second + } episodingCfg := episoding.EpisodingConfig{ EpisodeWindowSizeMin: cfg.Episoding.EpisodeWindowSizeMin, MinEventsPerEpisode: cfg.Episoding.MinEventsPerEpisode, - PollingInterval: 10 * time.Second, + PollingInterval: pollingInterval, + StartupLookback: cfg.Episoding.StartupLookback, + DefaultSalience: cfg.Episoding.DefaultSalience, } episodingAgent = episoding.NewEpisodingAgent(memStore, wrap("episoding"), log, episodingCfg) if err := episodingAgent.Start(rootCtx, bus); err != nil { @@ -1539,7 +1545,10 @@ func serveCommand(configPath string) { var metaAgent *metacognition.MetacognitionAgent if cfg.Metacognition.Enabled { metaAgent = metacognition.NewMetacognitionAgent(memStore, wrap("metacognition"), metacognition.MetacognitionConfig{ - Interval: cfg.Metacognition.Interval, + Interval: cfg.Metacognition.Interval, + StartupDelay: time.Duration(cfg.Metacognition.StartupDelaySec) * time.Second, + ReflectionLookback: cfg.Metacognition.ReflectionLookback, + DeadMemoryWindow: cfg.Metacognition.DeadMemoryWindow, }, log) if err := metaAgent.Start(rootCtx, bus); err != nil { @@ -1558,6 +1567,10 @@ func serveCommand(configPath string) { SalienceThreshold: cfg.Dreaming.SalienceThreshold, AssociationBoostFactor: cfg.Dreaming.AssociationBoostFactor, NoisePruneThreshold: cfg.Dreaming.NoisePruneThreshold, + StartupDelay: time.Duration(cfg.Dreaming.StartupDelaySec) * time.Second, + DeadMemoryWindow: cfg.Dreaming.DeadMemoryWindow, + InsightsBudget: cfg.Dreaming.InsightsBudget, + DefaultConfidence: cfg.Dreaming.DefaultConfidence, }, log) if err := dreamer.Start(rootCtx, bus); err != nil { @@ -1571,9 +1584,16 @@ func serveCommand(configPath string) { var abstractionAgent *abstraction.AbstractionAgent if cfg.Abstraction.Enabled { abstractionAgent = abstraction.NewAbstractionAgent(memStore, wrap("abstraction"), abstraction.AbstractionConfig{ - Interval: cfg.Abstraction.Interval, - MinStrength: cfg.Abstraction.MinStrength, - MaxLLMCalls: cfg.Abstraction.MaxLLMCalls, + Interval: cfg.Abstraction.Interval, + MinStrength: cfg.Abstraction.MinStrength, + MaxLLMCalls: cfg.Abstraction.MaxLLMCalls, + StartupDelay: time.Duration(cfg.Abstraction.StartupDelaySec) * time.Second, + DefaultConfidence: cfg.Abstraction.DefaultConfidence, + PatternAxiomConfidence: cfg.Abstraction.PatternAxiomConfidence, + ConfidenceModerateDecay: cfg.Abstraction.ConfidenceModerateDecay, + ConfidenceSignificantDecay: cfg.Abstraction.ConfidenceSignificantDecay, + ConfidenceSevereDecay: cfg.Abstraction.ConfidenceSevereDecay, + GroundingFloor: cfg.Abstraction.GroundingFloor, }, log) if err := abstractionAgent.Start(rootCtx, bus); err != nil { @@ -2502,7 +2522,7 @@ func formatDetailValue(val interface{}) string { // metaCycleCommand runs a single metacognition cycle and displays results. func metaCycleCommand(configPath string) { - _, db, llmProvider, log := initRuntime(configPath) + cfg, db, llmProvider, log := initRuntime(configPath) defer func() { _ = db.Close() }() ctx := context.Background() @@ -2510,7 +2530,9 @@ func metaCycleCommand(configPath string) { defer func() { _ = bus.Close() }() agent := metacognition.NewMetacognitionAgent(db, llmProvider, metacognition.MetacognitionConfig{ - Interval: 24 * time.Hour, // doesn't matter for RunOnce + Interval: 24 * time.Hour, // doesn't matter for RunOnce + ReflectionLookback: cfg.Metacognition.ReflectionLookback, + DeadMemoryWindow: cfg.Metacognition.DeadMemoryWindow, }, log) fmt.Println("Running metacognition cycle...") @@ -2567,6 +2589,9 @@ func dreamCycleCommand(configPath string) { SalienceThreshold: cfg.Dreaming.SalienceThreshold, AssociationBoostFactor: cfg.Dreaming.AssociationBoostFactor, NoisePruneThreshold: cfg.Dreaming.NoisePruneThreshold, + DeadMemoryWindow: cfg.Dreaming.DeadMemoryWindow, + InsightsBudget: cfg.Dreaming.InsightsBudget, + DefaultConfidence: cfg.Dreaming.DefaultConfidence, }, log) fmt.Println("Running dream cycle (memory replay)...") diff --git a/internal/agent/abstraction/agent.go b/internal/agent/abstraction/agent.go index 13362de7..6152dd9e 100644 --- a/internal/agent/abstraction/agent.go +++ b/internal/agent/abstraction/agent.go @@ -16,9 +16,16 @@ import ( ) type AbstractionConfig struct { - Interval time.Duration - MinStrength float32 - MaxLLMCalls int + Interval time.Duration + MinStrength float32 + MaxLLMCalls int + StartupDelay time.Duration + DefaultConfidence float32 + PatternAxiomConfidence float32 + ConfidenceModerateDecay float32 + ConfidenceSignificantDecay float32 + ConfidenceSevereDecay float32 + GroundingFloor float32 } type AbstractionAgent struct { @@ -94,8 +101,11 @@ func (aa *AbstractionAgent) RunOnce(ctx context.Context) (*CycleReport, error) { func (aa *AbstractionAgent) loop() { defer aa.wg.Done() - // 5-minute startup grace period (runs less frequently than other agents) - startupTimer := time.NewTimer(5 * time.Minute) + startupDelay := aa.config.StartupDelay + if startupDelay <= 0 { + startupDelay = 5 * time.Minute + } + startupTimer := time.NewTimer(startupDelay) defer startupTimer.Stop() ticker := time.NewTicker(aa.config.Interval) @@ -392,6 +402,24 @@ func (aa *AbstractionAgent) verifyGrounding(ctx context.Context, report *CycleRe continue } + // Load grounding multipliers from config with safe defaults + moderateDecay := aa.config.ConfidenceModerateDecay + if moderateDecay <= 0 { + moderateDecay = 0.9 + } + significantDecay := aa.config.ConfidenceSignificantDecay + if significantDecay <= 0 { + significantDecay = 0.7 + } + severeDecay := aa.config.ConfidenceSevereDecay + if severeDecay <= 0 { + severeDecay = 0.5 + } + groundingFloor := aa.config.GroundingFloor + if groundingFloor <= 0 { + groundingFloor = 0.5 + } + // Graduated grounding response switch { case groundingRatio >= 0.5: @@ -399,14 +427,14 @@ func (aa *AbstractionAgent) verifyGrounding(ctx context.Context, report *CycleRe continue case groundingRatio >= 0.3: // Moderate decay: reduce confidence slightly - abs.Confidence *= 0.9 + abs.Confidence *= moderateDecay case groundingRatio >= 0.1: // Significant decay: reduce confidence more - abs.Confidence *= 0.7 + abs.Confidence *= significantDecay report.AbstractionsDemoted++ default: - // Nearly all evidence gone: softened demotion (was 0.3, now 0.5) - abs.Confidence *= 0.5 + // Nearly all evidence gone + abs.Confidence *= severeDecay if abs.Confidence < 0.1 { abs.State = "fading" } @@ -414,8 +442,8 @@ func (aa *AbstractionAgent) verifyGrounding(ctx context.Context, report *CycleRe } // Enforce grace period floor for young abstractions - if isYoung && abs.Confidence < 0.5 { - abs.Confidence = 0.5 + if isYoung && abs.Confidence < groundingFloor { + abs.Confidence = groundingFloor } abs.UpdatedAt = time.Now() @@ -518,9 +546,13 @@ Set has_principle to false if: concepts = agentutil.DeduplicateConcepts(allConcepts) } + defaultConf := aa.config.DefaultConfidence + if defaultConf <= 0 { + defaultConf = 0.6 + } confidence := float32(result.Confidence) if confidence <= 0 || confidence > 1.0 { - confidence = 0.6 + confidence = defaultConf } return &store.Abstraction{ @@ -625,9 +657,13 @@ Set has_axiom to false if: concepts = agentutil.DeduplicateConcepts(allConcepts) } + axiomConf := aa.config.PatternAxiomConfidence + if axiomConf <= 0 { + axiomConf = 0.5 + } confidence := float32(result.Confidence) if confidence <= 0 || confidence > 1.0 { - confidence = 0.5 + confidence = axiomConf } return &store.Abstraction{ diff --git a/internal/agent/dreaming/agent.go b/internal/agent/dreaming/agent.go index 98ca301f..217ef2cb 100644 --- a/internal/agent/dreaming/agent.go +++ b/internal/agent/dreaming/agent.go @@ -22,6 +22,10 @@ type DreamingConfig struct { SalienceThreshold float32 AssociationBoostFactor float32 NoisePruneThreshold float32 + StartupDelay time.Duration + DeadMemoryWindow time.Duration + InsightsBudget int + DefaultConfidence float32 } type DreamingAgent struct { @@ -96,8 +100,11 @@ func (da *DreamingAgent) RunOnce(ctx context.Context) (*DreamReport, error) { func (da *DreamingAgent) loop() { defer da.wg.Done() - // 90-second startup grace period - startupTimer := time.NewTimer(90 * time.Second) + startupDelay := da.config.StartupDelay + if startupDelay <= 0 { + startupDelay = 90 * time.Second + } + startupTimer := time.NewTimer(startupDelay) defer startupTimer.Stop() ticker := time.NewTicker(da.config.Interval) @@ -313,7 +320,11 @@ func (da *DreamingAgent) crossPollinate(ctx context.Context, replayed []store.Me // noisePrune performs Phase 4: reduce salience of low-quality dead memories. func (da *DreamingAgent) noisePrune(ctx context.Context, report *DreamReport) error { - deadMemories, err := da.store.GetDeadMemories(ctx, time.Now().Add(-30*24*time.Hour)) + deadWindow := da.config.DeadMemoryWindow + if deadWindow <= 0 { + deadWindow = 30 * 24 * time.Hour + } + deadMemories, err := da.store.GetDeadMemories(ctx, time.Now().Add(-deadWindow)) if err != nil { return fmt.Errorf("failed to get dead memories: %w", err) } @@ -496,7 +507,10 @@ func (da *DreamingAgent) generateInsights(ctx context.Context, replayed []store. return nil } - insightsBudget := 2 + insightsBudget := da.config.InsightsBudget + if insightsBudget <= 0 { + insightsBudget = 2 + } for _, cluster := range clusters { if insightsBudget <= 0 { break @@ -641,9 +655,13 @@ Only share an insight if it's genuinely illuminating — something that makes yo concepts = agentutil.DeduplicateConcepts(allConcepts) } + defaultConf := da.config.DefaultConfidence + if defaultConf <= 0 { + defaultConf = 0.6 + } confidence := float32(result.Confidence) if confidence <= 0 || confidence > 1.0 { - confidence = 0.6 + confidence = defaultConf } abstraction := &store.Abstraction{ diff --git a/internal/agent/episoding/agent.go b/internal/agent/episoding/agent.go index b4414705..ec2ee549 100644 --- a/internal/agent/episoding/agent.go +++ b/internal/agent/episoding/agent.go @@ -21,6 +21,8 @@ type EpisodingConfig struct { EpisodeWindowSizeMin int // fixed window size in minutes (default 10) MinEventsPerEpisode int // minimum events to form an episode (default 2) PollingInterval time.Duration // how often to check for new events (default 10s) + StartupLookback time.Duration // how far back to look on startup (default 1h) + DefaultSalience float32 // fallback salience for synthesized episodes (default 0.5) } // DefaultEpisodingConfig returns sensible defaults. @@ -53,12 +55,16 @@ type EpisodingAgent struct { // NewEpisodingAgent creates a new episoding agent. func NewEpisodingAgent(s store.Store, llmProvider llm.Provider, log *slog.Logger, cfg EpisodingConfig) *EpisodingAgent { + lookback := cfg.StartupLookback + if lookback <= 0 { + lookback = 1 * time.Hour + } return &EpisodingAgent{ store: s, llmProvider: llmProvider, config: cfg, log: log, - lastProcessedTime: time.Now().Add(-1 * time.Hour), // look back 1 hour on start + lastProcessedTime: time.Now().Add(-lookback), assignedRawIDs: make(map[string]bool), } } @@ -67,6 +73,14 @@ func (ea *EpisodingAgent) Name() string { return "episoding" } +// defaultSalience returns the configured default salience, falling back to 0.5. +func (ea *EpisodingAgent) defaultSalience() float32 { + if ea.config.DefaultSalience > 0 { + return ea.config.DefaultSalience + } + return 0.5 +} + func (ea *EpisodingAgent) Start(ctx context.Context, bus events.Bus) error { ea.ctx, ea.cancel = context.WithCancel(ctx) ea.bus = bus @@ -412,7 +426,7 @@ Respond with ONLY a JSON object (no prose, no fences): ea.log.Warn("LLM episode synthesis failed, using fallback", "error", err) ep.Title = fmt.Sprintf("Session with %d events", len(ep.RawMemoryIDs)) ep.Summary = ep.Title - ep.Salience = 0.5 + ep.Salience = ea.defaultSalience() ep.Concepts = []string{} } else { // Parse LLM response diff --git a/internal/agent/metacognition/agent.go b/internal/agent/metacognition/agent.go index 64bdfe53..b45b90f6 100644 --- a/internal/agent/metacognition/agent.go +++ b/internal/agent/metacognition/agent.go @@ -14,7 +14,10 @@ import ( ) type MetacognitionConfig struct { - Interval time.Duration + Interval time.Duration + StartupDelay time.Duration + ReflectionLookback time.Duration + DeadMemoryWindow time.Duration } type MetacognitionAgent struct { @@ -84,7 +87,11 @@ func (ma *MetacognitionAgent) RunOnce(ctx context.Context) (*CycleReport, error) func (ma *MetacognitionAgent) loop() { defer ma.wg.Done() - startupTimer := time.NewTimer(60 * time.Second) + startupDelay := ma.config.StartupDelay + if startupDelay <= 0 { + startupDelay = 60 * time.Second + } + startupTimer := time.NewTimer(startupDelay) defer startupTimer.Stop() ticker := time.NewTicker(ma.config.Interval) @@ -116,8 +123,12 @@ func (ma *MetacognitionAgent) loop() { func (ma *MetacognitionAgent) runCycle(ctx context.Context) (*CycleReport, error) { startTime := time.Now() - // Cleanup: remove meta observations older than 7 days to prevent stale triggers - cutoff := time.Now().Add(-7 * 24 * time.Hour) + // Cleanup: remove meta observations older than reflection lookback to prevent stale triggers + lookback := ma.config.ReflectionLookback + if lookback <= 0 { + lookback = 7 * 24 * time.Hour + } + cutoff := time.Now().Add(-lookback) if deleted, err := ma.store.DeleteOldMetaObservations(ctx, cutoff); err != nil { ma.log.Warn("failed to cleanup old meta observations", "error", err) } else if deleted > 0 { @@ -277,7 +288,11 @@ func (ma *MetacognitionAgent) analyzeSourceDistribution(ctx context.Context) *st } func (ma *MetacognitionAgent) analyzeRecallEffectiveness(ctx context.Context) *store.MetaObservation { - deadMemories, err := ma.store.GetDeadMemories(ctx, time.Now().Add(-30*24*time.Hour)) + deadWindow := ma.config.DeadMemoryWindow + if deadWindow <= 0 { + deadWindow = 30 * 24 * time.Hour + } + deadMemories, err := ma.store.GetDeadMemories(ctx, time.Now().Add(-deadWindow)) if err != nil { ma.log.Error("failed to get dead memories", "error", err) return nil @@ -365,7 +380,11 @@ func (ma *MetacognitionAgent) checkConsolidationHealth(ctx context.Context) *sto // analyzeRetrievalFeedback reads actual retrieval feedback records and computes quality metrics. func (ma *MetacognitionAgent) analyzeRetrievalFeedback(ctx context.Context) *store.MetaObservation { - since := time.Now().Add(-7 * 24 * time.Hour) + feedbackLookback := ma.config.ReflectionLookback + if feedbackLookback <= 0 { + feedbackLookback = 7 * 24 * time.Hour + } + since := time.Now().Add(-feedbackLookback) feedbacks, err := ma.store.ListRecentRetrievalFeedback(ctx, since, 50) if err != nil { ma.log.Warn("failed to list recent retrieval feedback", "error", err) diff --git a/internal/config/config.go b/internal/config/config.go index 3fbb1e69..2c473d9b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -270,9 +270,14 @@ type RetrievalConfig struct { // MetacognitionConfig holds metacognition settings. type MetacognitionConfig struct { - Enabled bool `yaml:"enabled"` - IntervalRaw string `yaml:"interval"` - Interval time.Duration `yaml:"-"` + Enabled bool `yaml:"enabled"` + IntervalRaw string `yaml:"interval"` + Interval time.Duration `yaml:"-"` + StartupDelaySec int `yaml:"startup_delay_sec"` // seconds before first cycle (default: 60) + ReflectionLookbackRaw string `yaml:"reflection_lookback"` // how far back to analyze (default: "7d") + ReflectionLookback time.Duration `yaml:"-"` + DeadMemoryWindowRaw string `yaml:"dead_memory_window"` // age threshold for dead memory analysis (default: "30d") + DeadMemoryWindow time.Duration `yaml:"-"` } // DreamingConfig holds dreaming (memory replay) agent settings. @@ -284,13 +289,22 @@ type DreamingConfig struct { SalienceThreshold float32 `yaml:"salience_threshold"` AssociationBoostFactor float32 `yaml:"association_boost_factor"` NoisePruneThreshold float32 `yaml:"noise_prune_threshold"` + StartupDelaySec int `yaml:"startup_delay_sec"` // seconds before first cycle (default: 90) + DeadMemoryWindowRaw string `yaml:"dead_memory_window"` // age threshold for noise pruning (default: "30d") + DeadMemoryWindow time.Duration `yaml:"-"` + InsightsBudget int `yaml:"insights_budget"` // max insights per dream cycle (default: 2) + DefaultConfidence float32 `yaml:"default_confidence"` // fallback confidence for generated insights (default: 0.6) } // EpisodingConfig configures the episoding agent. type EpisodingConfig struct { - Enabled bool `yaml:"enabled"` - EpisodeWindowSizeMin int `yaml:"episode_window_size_min"` - MinEventsPerEpisode int `yaml:"min_events_per_episode"` + Enabled bool `yaml:"enabled"` + EpisodeWindowSizeMin int `yaml:"episode_window_size_min"` + MinEventsPerEpisode int `yaml:"min_events_per_episode"` + StartupLookbackRaw string `yaml:"startup_lookback"` // how far back to look on startup (default: "1h") + StartupLookback time.Duration `yaml:"-"` + DefaultSalience float32 `yaml:"default_salience"` // fallback salience for synthesized episodes (default: 0.5) + PollingIntervalSec int `yaml:"polling_interval_sec"` // seconds between episode checks (default: 10) } // AbstractionConfig configures the abstraction agent (hierarchical knowledge). @@ -300,6 +314,13 @@ type AbstractionConfig struct { Interval time.Duration `yaml:"-"` MinStrength float32 `yaml:"min_strength"` // minimum pattern strength to consider MaxLLMCalls int `yaml:"max_llm_calls"` // budget per cycle + StartupDelaySec int `yaml:"startup_delay_sec"` // seconds before first cycle (default: 300) + DefaultConfidence float32 `yaml:"default_confidence"` // fallback confidence for principles (default: 0.6) + PatternAxiomConfidence float32 `yaml:"pattern_axiom_confidence"` // fallback confidence for axioms (default: 0.5) + ConfidenceModerateDecay float32 `yaml:"confidence_moderate_decay"` // grounding multiplier for moderate decay (default: 0.9) + ConfidenceSignificantDecay float32 `yaml:"confidence_significant_decay"` // grounding multiplier for significant decay (default: 0.7) + ConfidenceSevereDecay float32 `yaml:"confidence_severe_decay"` // grounding multiplier for severe decay (default: 0.5) + GroundingFloor float32 `yaml:"grounding_floor"` // confidence floor for young abstractions (default: 0.5) } // OrchestratorConfig configures the autonomous orchestrator. @@ -612,9 +633,14 @@ func Default() *Config { }, }, Metacognition: MetacognitionConfig{ - Enabled: true, - IntervalRaw: "24h", - Interval: 24 * time.Hour, + Enabled: true, + IntervalRaw: "24h", + Interval: 24 * time.Hour, + StartupDelaySec: 60, + ReflectionLookbackRaw: "7d", + ReflectionLookback: 7 * 24 * time.Hour, + DeadMemoryWindowRaw: "30d", + DeadMemoryWindow: 30 * 24 * time.Hour, }, Dreaming: DreamingConfig{ Enabled: true, @@ -624,18 +650,34 @@ func Default() *Config { SalienceThreshold: 0.3, AssociationBoostFactor: 1.15, NoisePruneThreshold: 0.15, + StartupDelaySec: 90, + DeadMemoryWindowRaw: "30d", + DeadMemoryWindow: 30 * 24 * time.Hour, + InsightsBudget: 2, + DefaultConfidence: 0.6, }, Episoding: EpisodingConfig{ Enabled: true, EpisodeWindowSizeMin: 10, MinEventsPerEpisode: 2, + StartupLookbackRaw: "1h", + StartupLookback: 1 * time.Hour, + DefaultSalience: 0.5, + PollingIntervalSec: 10, }, Abstraction: AbstractionConfig{ - Enabled: true, - IntervalRaw: "6h", - Interval: 6 * time.Hour, - MinStrength: 0.4, - MaxLLMCalls: 5, + Enabled: true, + IntervalRaw: "6h", + Interval: 6 * time.Hour, + MinStrength: 0.4, + MaxLLMCalls: 5, + StartupDelaySec: 300, + DefaultConfidence: 0.6, + PatternAxiomConfidence: 0.5, + ConfidenceModerateDecay: 0.9, + ConfidenceSignificantDecay: 0.7, + ConfidenceSevereDecay: 0.5, + GroundingFloor: 0.5, }, Orchestrator: OrchestratorConfig{ Enabled: true, @@ -744,6 +786,10 @@ func (c *Config) process(configDir string) error { {c.Abstraction.IntervalRaw, &c.Abstraction.Interval, "abstraction.interval"}, {c.Orchestrator.SelfTestIntervalRaw, &c.Orchestrator.SelfTestInterval, "orchestrator.self_test_interval"}, {c.Orchestrator.MonitorIntervalRaw, &c.Orchestrator.MonitorInterval, "orchestrator.monitor_interval"}, + {c.Metacognition.ReflectionLookbackRaw, &c.Metacognition.ReflectionLookback, "metacognition.reflection_lookback"}, + {c.Metacognition.DeadMemoryWindowRaw, &c.Metacognition.DeadMemoryWindow, "metacognition.dead_memory_window"}, + {c.Dreaming.DeadMemoryWindowRaw, &c.Dreaming.DeadMemoryWindow, "dreaming.dead_memory_window"}, + {c.Episoding.StartupLookbackRaw, &c.Episoding.StartupLookback, "episoding.startup_lookback"}, } for _, d := range durations { if d.raw != "" {