diff --git a/.claude/hooks/protect-git.sh b/.claude/hooks/protect-git.sh index 055b3018..3d006f0e 100755 --- a/.claude/hooks/protect-git.sh +++ b/.claude/hooks/protect-git.sh @@ -15,7 +15,7 @@ if ! echo "$COMMAND" | grep -q 'git '; then fi # Block: git push --force or git push -f -if echo "$COMMAND" | grep -qE 'git push\s.*(-f|--force)'; then +if echo "$COMMAND" | grep -qE 'git push\s.*(\s-f\b|\s--force\b)'; then echo "BLOCKED: force push is not allowed. It can destroy remote branch history." >&2 exit 2 fi diff --git a/cmd/mnemonic/ingest.go b/cmd/mnemonic/ingest.go index dd5c05c9..22327db7 100644 --- a/cmd/mnemonic/ingest.go +++ b/cmd/mnemonic/ingest.go @@ -183,11 +183,11 @@ func ingestCommand(configPath string, args []string) { } raw := store.RawMemory{ - ID: uuid.New().String(), - Timestamp: time.Now(), - Source: "ingest", - Type: "file", - Content: fmt.Sprintf("File %s:\n%s", relPath, content), + ID: uuid.New().String(), + Timestamp: time.Now(), + Source: "ingest", + Type: "file", + Content: fmt.Sprintf("File %s:\n%s", relPath, content), Metadata: map[string]interface{}{ "path": path, "rel_path": relPath, diff --git a/internal/agent/consolidation/agent.go b/internal/agent/consolidation/agent.go index 70704af4..913aea18 100644 --- a/internal/agent/consolidation/agent.go +++ b/internal/agent/consolidation/agent.go @@ -211,20 +211,30 @@ func (ca *ConsolidationAgent) runCycle(ctx context.Context) (*CycleReport, error } report.AssociationsPruned = pruned + // Steps 4-5 require LLM — skip if unavailable to avoid timeout loops + llmAvailable := ca.llmProvider != nil && ca.llmProvider.Health(ctx) == nil + if !llmAvailable { + ca.log.Warn("skipping LLM-dependent steps (merge, pattern extraction): LLM unavailable") + } + // Step 4: Merge highly similar memory clusters into gists - merges, err := ca.mergeClusters(ctx) - if err != nil { - ca.log.Warn("cluster merging failed", "error", err) - // Non-fatal, continue + if llmAvailable { + merges, err := ca.mergeClusters(ctx) + if err != nil { + ca.log.Warn("cluster merging failed", "error", err) + // Non-fatal, continue + } + report.MergesPerformed = merges } - report.MergesPerformed = merges // Step 5: Extract patterns from memory clusters - patternsExtracted, err := ca.extractPatterns(ctx) - if err != nil { - ca.log.Warn("pattern extraction failed", "error", err) + if llmAvailable { + patternsExtracted, err := ca.extractPatterns(ctx) + if err != nil { + ca.log.Warn("pattern extraction failed", "error", err) + } + report.PatternsExtracted = patternsExtracted } - report.PatternsExtracted = patternsExtracted // Step 6: Delete expired archived memories deleted, err := ca.deleteExpired(ctx) @@ -284,12 +294,12 @@ func (ca *ConsolidationAgent) decaySalience(ctx context.Context) (decayed, proce hoursSinceAccess = time.Since(mem.CreatedAt).Hours() } - // Recency protection: 0-24h = 0.5x decay, 24-168h = 0.8x decay, >168h = full decay + // Recency protection: 0-24h = 0.8x decay, 24-168h = 0.9x decay, >168h = full decay recencyFactor := 1.0 if hoursSinceAccess < 24 { - recencyFactor = 0.5 - } else if hoursSinceAccess < 168 { // 7 days recencyFactor = 0.8 + } else if hoursSinceAccess < 168 { // 7 days + recencyFactor = 0.9 } // Access count bonus: frequently accessed memories resist decay @@ -903,10 +913,10 @@ func (ca *ConsolidationAgent) findMatchingPattern(ctx context.Context, cluster [ return nil, fmt.Errorf("no matching patterns") } - // Check if the top match is close enough (0.8 threshold) + // Check if the top match is close enough (0.70 threshold) if len(patterns[0].Embedding) > 0 { sim := cosineSimilarity(avgEmb, patterns[0].Embedding) - if sim >= 0.8 { + if sim >= 0.70 { return &patterns[0], nil } } diff --git a/internal/agent/consolidation/agent_test.go b/internal/agent/consolidation/agent_test.go index da861e8e..601cc93a 100644 --- a/internal/agent/consolidation/agent_test.go +++ b/internal/agent/consolidation/agent_test.go @@ -691,9 +691,9 @@ func TestDecaySalience(t *testing.T) { updates := ms.batchUpdateSalienceCalls[0] newSalience := updates["recent-mem"] - // recencyFactor=0.5, accessBonus=1.0, effective = 0.95^0.5 ≈ 0.9747 - // newSalience = 0.8 * 0.9747 ≈ 0.7798 - effectiveDecay := math.Pow(0.95, 0.5) + // recencyFactor=0.8, accessBonus=1.0, effective = 0.95^0.8 ≈ 0.9592 + // newSalience = 0.8 * 0.9592 ≈ 0.7674 + effectiveDecay := math.Pow(0.95, 0.8) expectedSalience := float32(0.8 * effectiveDecay) if !almostEqual(newSalience, expectedSalience, 0.01) { t.Errorf("expected salience ~%f for recently accessed memory, got %f", expectedSalience, newSalience) diff --git a/internal/agent/dreaming/agent.go b/internal/agent/dreaming/agent.go index 7e52b951..895bdd6f 100644 --- a/internal/agent/dreaming/agent.go +++ b/internal/agent/dreaming/agent.go @@ -39,7 +39,7 @@ type DreamingAgent struct { type DreamReport struct { Duration time.Duration MemoriesReplayed int - AssociationsStrengthened int + AssociationsStrengthened int NewAssociationsCreated int CrossProjectLinks int PatternLinks int @@ -137,6 +137,15 @@ func (da *DreamingAgent) loop() { } func (da *DreamingAgent) runCycle(ctx context.Context) (*DreamReport, error) { + // Gate on LLM availability — without LLM, dreaming blindly strengthens + // associations without being able to generate insights or judge quality. + if da.llmProvider != nil { + if err := da.llmProvider.Health(ctx); err != nil { + da.log.Warn("skipping dream cycle: LLM unavailable", "error", err) + return nil, nil + } + } + startTime := time.Now() report := &DreamReport{} @@ -544,11 +553,11 @@ func clusterByConceptOverlap(memories []store.Memory) [][]store.Memory { } type insightResponse struct { - Title string `json:"title"` - Insight string `json:"insight"` - Concepts []string `json:"concepts"` - Confidence float64 `json:"confidence"` - HasInsight bool `json:"has_insight"` + Title string `json:"title"` + Insight string `json:"insight"` + Concepts []string `json:"concepts"` + Confidence float64 `json:"confidence"` + HasInsight bool `json:"has_insight"` } // synthesizeInsight asks the LLM to identify a higher-order insight from a cluster of memories. diff --git a/internal/agent/encoding/agent.go b/internal/agent/encoding/agent.go index a349d7b3..4181bc11 100644 --- a/internal/agent/encoding/agent.go +++ b/internal/agent/encoding/agent.go @@ -24,21 +24,21 @@ const maxRetries = 3 // EncodingAgent transforms raw memories into encoded, searchable memory units. // It performs compression, concept extraction, embedding generation, and association creation. type EncodingAgent struct { - store store.Store - llmProvider llm.Provider - log *slog.Logger - bus events.Bus - config EncodingConfig - name string - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - subscriptionID string - pollingStopChan chan struct{} - stopOnce sync.Once - processingMutex sync.Mutex - processingMemories map[string]bool // Prevent duplicate processing - encodingSem chan struct{} // limits concurrent LLM encoding calls + store store.Store + llmProvider llm.Provider + log *slog.Logger + bus events.Bus + config EncodingConfig + name string + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + subscriptionID string + pollingStopChan chan struct{} + stopOnce sync.Once + processingMutex sync.Mutex + processingMemories map[string]bool // Prevent duplicate processing + encodingSem chan struct{} // limits concurrent LLM encoding calls failureCounts map[string]int // tracks retry count per raw memory ID backoffUntil time.Time // when non-zero, skip polling until this time coachingInstructions string // loaded from coaching.yaml at startup @@ -80,7 +80,7 @@ type compressionResponse struct { Content string `json:"content"` Narrative string `json:"narrative"` Concepts []string `json:"concepts"` - StructuredConcepts *structuredConcepts `json:"structured_concepts"` + StructuredConcepts *structuredConcepts `json:"structured_concepts"` Significance string `json:"significance"` EmotionalTone string `json:"emotional_tone"` Outcome string `json:"outcome"` @@ -88,10 +88,10 @@ type compressionResponse struct { } type structuredConcepts struct { - Topics []topicEntry `json:"topics"` - Entities []entityEntry `json:"entities"` - Actions []actionEntry `json:"actions"` - Causality []causalEntry `json:"causality"` + Topics []topicEntry `json:"topics"` + Entities []entityEntry `json:"entities"` + Actions []actionEntry `json:"actions"` + Causality []causalEntry `json:"causality"` } type topicEntry struct { @@ -1095,12 +1095,12 @@ func looksLikeWord(s string) bool { // validRelationTypes lists all valid association relationship types. var validRelationTypes = map[string]bool{ - "similar": true, - "caused_by": true, - "part_of": true, + "similar": true, + "caused_by": true, + "part_of": true, "contradicts": true, - "temporal": true, - "reinforces": true, + "temporal": true, + "reinforces": true, } // classifyRelationship determines the relationship type between a new memory and an existing one. diff --git a/internal/agent/metacognition/agent.go b/internal/agent/metacognition/agent.go index a69d3a3d..c376d2ec 100644 --- a/internal/agent/metacognition/agent.go +++ b/internal/agent/metacognition/agent.go @@ -7,10 +7,10 @@ import ( "sync" "time" - "github.com/google/uuid" "github.com/appsprout/mnemonic/internal/events" "github.com/appsprout/mnemonic/internal/llm" "github.com/appsprout/mnemonic/internal/store" + "github.com/google/uuid" ) type MetacognitionConfig struct { @@ -221,11 +221,11 @@ func (ma *MetacognitionAgent) auditMemoryQuality(ctx context.Context) *store.Met ObservationType: "quality_audit", Severity: severity, Details: map[string]interface{}{ - "no_embedding": noEmbedding, - "no_compression": noCompression, - "short_summary": shortSummary, - "long_summary": longSummary, - "total_issues": totalIssues, + "no_embedding": noEmbedding, + "no_compression": noCompression, + "short_summary": shortSummary, + "long_summary": longSummary, + "total_issues": totalIssues, }, } } @@ -263,9 +263,9 @@ func (ma *MetacognitionAgent) analyzeSourceDistribution(ctx context.Context) *st ObservationType: "source_balance", Severity: "warning", Details: map[string]interface{}{ - "source_counts": distribution, - "dominant_source": dominantSource, - "dominant_ratio": dominantRatio, + "source_counts": distribution, + "dominant_source": dominantSource, + "dominant_ratio": dominantRatio, }, } } @@ -500,8 +500,8 @@ func (ma *MetacognitionAgent) actOnQualityIssues(ctx context.Context, obs store. ObservationType: "autonomous_action", Severity: "info", Details: map[string]interface{}{ - "action": "re_embedded_memories", - "count": reembedded, + "action": "re_embedded_memories", + "count": reembedded, }, CreatedAt: time.Now(), } diff --git a/internal/agent/orchestrator/orchestrator.go b/internal/agent/orchestrator/orchestrator.go index 9265752a..8bea6556 100644 --- a/internal/agent/orchestrator/orchestrator.go +++ b/internal/agent/orchestrator/orchestrator.go @@ -17,28 +17,28 @@ import ( // OrchestratorConfig configures the autonomous orchestrator. type OrchestratorConfig struct { - AdaptiveIntervals bool - MaxDBSizeMB int - SelfTestInterval time.Duration - AutoRecovery bool - HealthReportPath string // e.g. "~/.mnemonic/health.json" - MonitorInterval time.Duration + AdaptiveIntervals bool + MaxDBSizeMB int + SelfTestInterval time.Duration + AutoRecovery bool + HealthReportPath string // e.g. "~/.mnemonic/health.json" + MonitorInterval time.Duration } // HealthReport is the machine-readable health status written periodically. type HealthReport struct { - Timestamp time.Time `json:"timestamp"` - Uptime string `json:"uptime"` - LLMAvailable bool `json:"llm_available"` - StoreHealthy bool `json:"store_healthy"` - MemoryCount int `json:"memory_count"` - PatternCount int `json:"pattern_count"` - AbstractionCount int `json:"abstraction_count"` - AgentStatus map[string]string `json:"agent_status"` - LastConsolidation string `json:"last_consolidation"` - LastDreamCycle string `json:"last_dream_cycle"` - AutonomousActions int `json:"autonomous_actions_total"` - Warnings []string `json:"warnings,omitempty"` + Timestamp time.Time `json:"timestamp"` + Uptime string `json:"uptime"` + LLMAvailable bool `json:"llm_available"` + StoreHealthy bool `json:"store_healthy"` + MemoryCount int `json:"memory_count"` + PatternCount int `json:"pattern_count"` + AbstractionCount int `json:"abstraction_count"` + AgentStatus map[string]string `json:"agent_status"` + LastConsolidation string `json:"last_consolidation"` + LastDreamCycle string `json:"last_dream_cycle"` + AutonomousActions int `json:"autonomous_actions_total"` + Warnings []string `json:"warnings,omitempty"` } // Orchestrator is the central autonomous scheduler and health monitor. @@ -383,9 +383,9 @@ func (o *Orchestrator) writeHealthReport() { AutonomousActions: o.autonomousCount, Warnings: append([]string{}, o.warnings...), AgentStatus: map[string]string{ - "orchestrator": "running", - "total_active": fmt.Sprintf("%d", stats.ActiveMemories), - "total_fading": fmt.Sprintf("%d", stats.FadingMemories), + "orchestrator": "running", + "total_active": fmt.Sprintf("%d", stats.ActiveMemories), + "total_fading": fmt.Sprintf("%d", stats.FadingMemories), }, } o.mu.Unlock() diff --git a/internal/agent/perception/agent.go b/internal/agent/perception/agent.go index e652a5a9..17377283 100644 --- a/internal/agent/perception/agent.go +++ b/internal/agent/perception/agent.go @@ -23,19 +23,19 @@ type PerceptionConfig struct { // PerceptionAgent orchestrates the perception pipeline: watchers → heuristic → LLM → memory. type PerceptionAgent struct { - name string - watchers []watcher.Watcher - store store.Store - llmProvider llm.Provider - cfg PerceptionConfig - log *slog.Logger - heuristicFilter *HeuristicFilter - bus events.Bus - mu sync.RWMutex - running bool - cancelFunc context.CancelFunc - processingWg sync.WaitGroup - watcherStopChans []chan struct{} // one per watcher goroutine + name string + watchers []watcher.Watcher + store store.Store + llmProvider llm.Provider + cfg PerceptionConfig + log *slog.Logger + heuristicFilter *HeuristicFilter + bus events.Bus + mu sync.RWMutex + running bool + cancelFunc context.CancelFunc + processingWg sync.WaitGroup + watcherStopChans []chan struct{} // one per watcher goroutine } // NewPerceptionAgent creates a new perception agent with the given dependencies. @@ -238,16 +238,16 @@ func (pa *PerceptionAgent) processEvent(ctx context.Context, event Event) { // 3. Create a raw memory entry rawMemory := store.RawMemory{ - ID: uuid.New().String(), - Source: event.Source, - Type: event.Type, - Content: pa.truncateContent(event.Content, 10000), - Timestamp: event.Timestamp, - CreatedAt: time.Now(), - Metadata: pa.mergeMetadata(event.Metadata, event.Path, heuristicResult.Score), - HeuristicScore: heuristicResult.Score, - InitialSalience: salience, - Processed: false, + ID: uuid.New().String(), + Source: event.Source, + Type: event.Type, + Content: pa.truncateContent(event.Content, 10000), + Timestamp: event.Timestamp, + CreatedAt: time.Now(), + Metadata: pa.mergeMetadata(event.Metadata, event.Path, heuristicResult.Score), + HeuristicScore: heuristicResult.Score, + InitialSalience: salience, + Processed: false, } // 4. Write to store diff --git a/internal/agent/perception/heuristic.go b/internal/agent/perception/heuristic.go index 76222de0..aa7f235f 100644 --- a/internal/agent/perception/heuristic.go +++ b/internal/agent/perception/heuristic.go @@ -252,6 +252,24 @@ func (h *HeuristicFilter) evaluateFilesystem(path, content string) (float32, str } } + // Suppress application-internal state directories — these produce high-volume + // noise that is never useful for memory (browser storage, desktop state, etc.) + appInternalDirs := []string{ + "/google-chrome/", "/chromium/", "/BraveSoftware/", + "/LM Studio/", "/lm-studio/", + "/Trash/", "/.local/share/Trash/", + "/leveldb/", "/IndexedDB/", "/Local Storage/", "/Session Storage/", + "/Cache/", "/GPUCache/", "/ShaderCache/", "/Code Cache/", + "/dconf/", "/gconf/", + "/pulse/", "/pipewire/", + } + lowerPathCheck := strings.ToLower(path) + for _, dir := range appInternalDirs { + if strings.Contains(lowerPathCheck, strings.ToLower(dir)) { + return 0.0, fmt.Sprintf("filesystem: application-internal path '%s'", dir) + } + } + score := float32(0.3) rationale := "filesystem event" diff --git a/internal/agent/retrieval/agent.go b/internal/agent/retrieval/agent.go index d33d728b..871c829b 100644 --- a/internal/agent/retrieval/agent.go +++ b/internal/agent/retrieval/agent.go @@ -894,4 +894,3 @@ func (ra *RetrievalAgent) applyFilters(results []store.RetrievalResult, req Quer } return filtered } - diff --git a/internal/api/routes/feedback.go b/internal/api/routes/feedback.go index 25010166..0d410547 100644 --- a/internal/api/routes/feedback.go +++ b/internal/api/routes/feedback.go @@ -7,8 +7,8 @@ import ( "net/http" "time" - "github.com/google/uuid" "github.com/appsprout/mnemonic/internal/store" + "github.com/google/uuid" ) const ( @@ -175,4 +175,3 @@ func SaveRetrievalFeedback(s store.Store, log *slog.Logger, queryID string, quer log.Debug("saved retrieval feedback record", "query_id", queryID, "retrieved", len(retrievedIDs), "traversed", len(traversedAssocs)) } } - diff --git a/internal/api/routes/graph.go b/internal/api/routes/graph.go index 504c218a..129697b1 100644 --- a/internal/api/routes/graph.go +++ b/internal/api/routes/graph.go @@ -12,16 +12,16 @@ import ( ) type GraphNode struct { - ID string `json:"id"` - Summary string `json:"summary"` - Salience float32 `json:"salience"` - State string `json:"state"` - Concepts []string `json:"concepts"` - EmotionalTone string `json:"emotional_tone,omitempty"` - Significance string `json:"significance,omitempty"` - Timestamp string `json:"timestamp"` - FilesModified []string `json:"files_modified,omitempty"` - EventCount int `json:"event_count,omitempty"` + ID string `json:"id"` + Summary string `json:"summary"` + Salience float32 `json:"salience"` + State string `json:"state"` + Concepts []string `json:"concepts"` + EmotionalTone string `json:"emotional_tone,omitempty"` + Significance string `json:"significance,omitempty"` + Timestamp string `json:"timestamp"` + FilesModified []string `json:"files_modified,omitempty"` + EventCount int `json:"event_count,omitempty"` } type GraphEdge struct { diff --git a/internal/api/routes/memories.go b/internal/api/routes/memories.go index be7c7d0e..41f2a26c 100644 --- a/internal/api/routes/memories.go +++ b/internal/api/routes/memories.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/google/uuid" "github.com/appsprout/mnemonic/internal/events" "github.com/appsprout/mnemonic/internal/store" + "github.com/google/uuid" ) // HandleGetRawMemory returns a single raw memory by ID. @@ -102,12 +102,12 @@ func HandleCreateMemory(s store.Store, bus events.Bus, log *slog.Logger) http.Ha } rawMem := store.RawMemory{ - ID: uuid.New().String(), - Timestamp: now, - Source: req.Source, - Type: req.Type, - Content: req.Content, - Project: req.Project, + ID: uuid.New().String(), + Timestamp: now, + Source: req.Source, + Type: req.Type, + Content: req.Content, + Project: req.Project, Metadata: map[string]interface{}{ "memory_type": req.Type, "project": req.Project, diff --git a/internal/api/routes/system.go b/internal/api/routes/system.go index 63030269..fafa6913 100644 --- a/internal/api/routes/system.go +++ b/internal/api/routes/system.go @@ -79,8 +79,8 @@ func HandleHealth(s store.Store, llmProv llm.Provider, log *slog.Logger) http.Ha // StatsResponse is the JSON response for the stats endpoint. type StatsResponse struct { - Store store.StoreStatistics `json:"store"` - Timestamp string `json:"timestamp"` + Store store.StoreStatistics `json:"store"` + Timestamp string `json:"timestamp"` } // HandleStats returns an HTTP handler that returns system statistics. diff --git a/internal/api/routes/ws.go b/internal/api/routes/ws.go index a08eae8a..7249abbf 100644 --- a/internal/api/routes/ws.go +++ b/internal/api/routes/ws.go @@ -7,8 +7,8 @@ import ( "sync/atomic" "time" - "github.com/gorilla/websocket" "github.com/appsprout/mnemonic/internal/events" + "github.com/gorilla/websocket" ) const maxWebSocketConns = 10 @@ -24,19 +24,19 @@ type WebSocketMessage struct { // wsConn wraps a WebSocket connection with subscription management. type wsConn struct { - conn *websocket.Conn - subscriptionIDs []string - eventChan chan events.Event - log *slog.Logger + conn *websocket.Conn + subscriptionIDs []string + eventChan chan events.Event + log *slog.Logger } // allowedWSOrigins is the set of origins allowed to open WebSocket connections. var allowedWSOrigins = map[string]bool{ - "http://localhost:3000": true, - "http://localhost:8080": true, + "http://localhost:3000": true, + "http://localhost:8080": true, "http://127.0.0.1:3000": true, "http://127.0.0.1:8080": true, - "http://localhost:9999": true, + "http://localhost:9999": true, "http://127.0.0.1:9999": true, } diff --git a/internal/api/server.go b/internal/api/server.go index 81d6df45..9b51855e 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -29,8 +29,8 @@ type ServerDeps struct { Bus events.Bus Retriever *retrieval.RetrievalAgent Consolidator routes.ConsolidationRunner // can be nil if disabled - AgentEvolutionDir string // empty = agent dashboard disabled - AgentWebPort int // 0 = agent chat disabled + AgentEvolutionDir string // empty = agent dashboard disabled + AgentWebPort int // 0 = agent chat disabled Log *slog.Logger } @@ -122,11 +122,11 @@ func (s *Server) registerRoutes() { // allowedCORSOrigins is the set of origins allowed for CORS requests. var allowedCORSOrigins = map[string]bool{ - "http://localhost:3000": true, - "http://localhost:8080": true, + "http://localhost:3000": true, + "http://localhost:8080": true, "http://127.0.0.1:3000": true, "http://127.0.0.1:8080": true, - "http://localhost:9999": true, + "http://localhost:9999": true, "http://127.0.0.1:9999": true, } diff --git a/internal/backup/export.go b/internal/backup/export.go index 933edecc..3d698ff1 100644 --- a/internal/backup/export.go +++ b/internal/backup/export.go @@ -20,18 +20,18 @@ const ( ) type ExportMetadata struct { - Version string `json:"version"` - ExportTime time.Time `json:"export_time"` - MemoryCount int `json:"memory_count"` - AssocCount int `json:"association_count"` - RawCount int `json:"raw_memory_count"` + Version string `json:"version"` + ExportTime time.Time `json:"export_time"` + MemoryCount int `json:"memory_count"` + AssocCount int `json:"association_count"` + RawCount int `json:"raw_memory_count"` } type ExportData struct { - Metadata ExportMetadata `json:"metadata"` - Memories []store.Memory `json:"memories"` - Associations []store.Association `json:"associations"` - RawMemories []store.RawMemory `json:"raw_memories"` + Metadata ExportMetadata `json:"metadata"` + Memories []store.Memory `json:"memories"` + Associations []store.Association `json:"associations"` + RawMemories []store.RawMemory `json:"raw_memories"` } func ExportJSON(ctx context.Context, s store.Store, outputPath string) error { @@ -52,11 +52,11 @@ func ExportJSON(ctx context.Context, s store.Store, outputPath string) error { exportData := ExportData{ Metadata: ExportMetadata{ - Version: "0.3.0", - ExportTime: time.Now(), - MemoryCount: len(memories), - AssocCount: len(associations), - RawCount: len(rawMemories), + Version: "0.3.0", + ExportTime: time.Now(), + MemoryCount: len(memories), + AssocCount: len(associations), + RawCount: len(rawMemories), }, Memories: memories, Associations: associations, diff --git a/internal/config/config.go b/internal/config/config.go index 730bc4f5..e5f761fe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,35 +15,35 @@ import ( // Config is the root configuration structure. type Config struct { - LLM LLMConfig `yaml:"llm"` - Store StoreConfig `yaml:"store"` - Memory MemoryConfig `yaml:"memory"` - Perception PerceptionConfig `yaml:"perception"` - Encoding EncodingConfig `yaml:"encoding"` - Consolidation ConsolidationConfig `yaml:"consolidation"` - Retrieval RetrievalConfig `yaml:"retrieval"` - Metacognition MetacognitionConfig `yaml:"metacognition"` - Dreaming DreamingConfig `yaml:"dreaming"` - Episoding EpisodingConfig `yaml:"episoding"` - Abstraction AbstractionConfig `yaml:"abstraction"` - Orchestrator OrchestratorConfig `yaml:"orchestrator"` - MCP MCPConfig `yaml:"mcp"` - AgentSDK AgentSDKConfig `yaml:"agent_sdk"` - Coaching CoachingConfig `yaml:"coaching"` - API APIConfig `yaml:"api"` - Web WebConfig `yaml:"web"` - Logging LoggingConfig `yaml:"logging"` + LLM LLMConfig `yaml:"llm"` + Store StoreConfig `yaml:"store"` + Memory MemoryConfig `yaml:"memory"` + Perception PerceptionConfig `yaml:"perception"` + Encoding EncodingConfig `yaml:"encoding"` + Consolidation ConsolidationConfig `yaml:"consolidation"` + Retrieval RetrievalConfig `yaml:"retrieval"` + Metacognition MetacognitionConfig `yaml:"metacognition"` + Dreaming DreamingConfig `yaml:"dreaming"` + Episoding EpisodingConfig `yaml:"episoding"` + Abstraction AbstractionConfig `yaml:"abstraction"` + Orchestrator OrchestratorConfig `yaml:"orchestrator"` + MCP MCPConfig `yaml:"mcp"` + AgentSDK AgentSDKConfig `yaml:"agent_sdk"` + Coaching CoachingConfig `yaml:"coaching"` + API APIConfig `yaml:"api"` + Web WebConfig `yaml:"web"` + Logging LoggingConfig `yaml:"logging"` } // LLMConfig holds LLM provider settings. type LLMConfig struct { - Endpoint string `yaml:"endpoint"` - ChatModel string `yaml:"chat_model"` - EmbeddingModel string `yaml:"embedding_model"` - MaxTokens int `yaml:"max_tokens"` + Endpoint string `yaml:"endpoint"` + ChatModel string `yaml:"chat_model"` + EmbeddingModel string `yaml:"embedding_model"` + MaxTokens int `yaml:"max_tokens"` Temperature float64 `yaml:"temperature"` - TimeoutSec int `yaml:"timeout_sec"` - MaxConcurrent int `yaml:"max_concurrent"` // max simultaneous LLM requests (0 = default 2) + TimeoutSec int `yaml:"timeout_sec"` + MaxConcurrent int `yaml:"max_concurrent"` // max simultaneous LLM requests (0 = default 2) } // StoreConfig holds storage settings. @@ -59,8 +59,8 @@ type MemoryConfig struct { // PerceptionConfig holds perception settings. type PerceptionConfig struct { - Enabled bool `yaml:"enabled"` - LLMGatingEnabled bool `yaml:"llm_gating_enabled"` + Enabled bool `yaml:"enabled"` + LLMGatingEnabled bool `yaml:"llm_gating_enabled"` Filesystem FilesystemPerceptionConfig `yaml:"filesystem"` Terminal TerminalPerceptionConfig `yaml:"terminal"` Clipboard ClipboardPerceptionConfig `yaml:"clipboard"` @@ -69,10 +69,10 @@ type PerceptionConfig struct { // FilesystemPerceptionConfig holds filesystem perception settings. type FilesystemPerceptionConfig struct { - Enabled bool `yaml:"enabled"` - WatchDirs []string `yaml:"watch_dirs"` - ExcludePatterns []string `yaml:"exclude_patterns"` - MaxContentBytes int `yaml:"max_content_bytes"` + Enabled bool `yaml:"enabled"` + WatchDirs []string `yaml:"watch_dirs"` + ExcludePatterns []string `yaml:"exclude_patterns"` + MaxContentBytes int `yaml:"max_content_bytes"` } // TerminalPerceptionConfig holds terminal perception settings. @@ -85,47 +85,47 @@ type TerminalPerceptionConfig struct { // ClipboardPerceptionConfig holds clipboard perception settings. type ClipboardPerceptionConfig struct { - Enabled bool `yaml:"enabled"` - PollIntervalSec int `yaml:"poll_interval_sec"` - MaxContentBytes int `yaml:"max_content_bytes"` + Enabled bool `yaml:"enabled"` + PollIntervalSec int `yaml:"poll_interval_sec"` + MaxContentBytes int `yaml:"max_content_bytes"` } // HeuristicsConfig holds heuristics settings. type HeuristicsConfig struct { - MinContentLength int `yaml:"min_content_length"` - MaxContentLength int `yaml:"max_content_length"` + MinContentLength int `yaml:"min_content_length"` + MaxContentLength int `yaml:"max_content_length"` FrequencyThreshold int `yaml:"frequency_threshold"` FrequencyWindowMin int `yaml:"frequency_window_min"` } // EncodingConfig holds encoding settings. type EncodingConfig struct { - Enabled bool `yaml:"enabled"` - UseLLM bool `yaml:"use_llm"` - MaxLLMQueueSize int `yaml:"max_llm_queue_size"` - MaxConcepts int `yaml:"max_concepts"` - FindSimilarLimit int `yaml:"find_similar_limit"` - EnableContextualEncoding bool `yaml:"enable_contextual_encoding"` - ContextLookbackCount int `yaml:"context_lookback_count"` - ContextSemanticCount int `yaml:"context_semantic_count"` - MaxConcurrentEncodings int `yaml:"max_concurrent_encodings"` - EnableLLMClassification bool `yaml:"enable_llm_classification"` - CompletionMaxTokens int `yaml:"completion_max_tokens"` + Enabled bool `yaml:"enabled"` + UseLLM bool `yaml:"use_llm"` + MaxLLMQueueSize int `yaml:"max_llm_queue_size"` + MaxConcepts int `yaml:"max_concepts"` + FindSimilarLimit int `yaml:"find_similar_limit"` + EnableContextualEncoding bool `yaml:"enable_contextual_encoding"` + ContextLookbackCount int `yaml:"context_lookback_count"` + ContextSemanticCount int `yaml:"context_semantic_count"` + MaxConcurrentEncodings int `yaml:"max_concurrent_encodings"` + EnableLLMClassification bool `yaml:"enable_llm_classification"` + CompletionMaxTokens int `yaml:"completion_max_tokens"` } // ConsolidationConfig holds consolidation settings. type ConsolidationConfig struct { - Enabled bool `yaml:"enabled"` - IntervalRaw string `yaml:"interval"` + Enabled bool `yaml:"enabled"` + IntervalRaw string `yaml:"interval"` Interval time.Duration `yaml:"-"` - DecayRate float64 `yaml:"decay_rate"` - FadeThreshold float64 `yaml:"fade_threshold"` - ArchiveThreshold float64 `yaml:"archive_threshold"` - RetentionWindowRaw string `yaml:"retention_window"` + DecayRate float64 `yaml:"decay_rate"` + FadeThreshold float64 `yaml:"fade_threshold"` + ArchiveThreshold float64 `yaml:"archive_threshold"` + RetentionWindowRaw string `yaml:"retention_window"` RetentionWindow time.Duration `yaml:"-"` - MaxMemoriesPerCycle int `yaml:"max_memories_per_cycle"` - MaxMergesPerCycle int `yaml:"max_merges_per_cycle"` - MinClusterSize int `yaml:"min_cluster_size"` + MaxMemoriesPerCycle int `yaml:"max_memories_per_cycle"` + MaxMergesPerCycle int `yaml:"max_merges_per_cycle"` + MinClusterSize int `yaml:"min_cluster_size"` } // RetrievalConfig holds retrieval settings. @@ -167,11 +167,11 @@ type EpisodingConfig struct { // AbstractionConfig configures the abstraction agent (hierarchical knowledge). type AbstractionConfig struct { - Enabled bool `yaml:"enabled"` - IntervalRaw string `yaml:"interval"` - Interval time.Duration `yaml:"-"` - MinStrength float32 `yaml:"min_strength"` // minimum pattern strength to consider - MaxLLMCalls int `yaml:"max_llm_calls"` // budget per cycle + Enabled bool `yaml:"enabled"` + IntervalRaw string `yaml:"interval"` + Interval time.Duration `yaml:"-"` + MinStrength float32 `yaml:"min_strength"` // minimum pattern strength to consider + MaxLLMCalls int `yaml:"max_llm_calls"` // budget per cycle } // OrchestratorConfig configures the autonomous orchestrator. @@ -201,9 +201,9 @@ type AgentSDKConfig struct { // APIConfig holds API server settings. type APIConfig struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - RequestTimeoutSec int `yaml:"request_timeout_sec"` + Host string `yaml:"host"` + Port int `yaml:"port"` + RequestTimeoutSec int `yaml:"request_timeout_sec"` } // WebConfig holds web UI settings. @@ -311,8 +311,8 @@ func Default() *Config { MaxContentBytes: 102400, }, Heuristics: HeuristicsConfig{ - MinContentLength: 10, - MaxContentLength: 100000, + MinContentLength: 10, + MaxContentLength: 100000, FrequencyThreshold: 5, FrequencyWindowMin: 10, }, diff --git a/internal/events/inmemory_test.go b/internal/events/inmemory_test.go index 58edbacb..2b6e0725 100644 --- a/internal/events/inmemory_test.go +++ b/internal/events/inmemory_test.go @@ -134,11 +134,11 @@ func TestUnsubscribe(t *testing.T) { }) event := QueryExecuted{ - QueryID: "q1", - QueryText: "test", + QueryID: "q1", + QueryText: "test", ResultsReturned: 5, - TookMs: 10, - Ts: time.Now(), + TookMs: 10, + Ts: time.Now(), } // Publish with both subscribers active @@ -328,12 +328,12 @@ func TestHandlerError(t *testing.T) { }) event := DreamCycleCompleted{ - MemoriesReplayed: 5, + MemoriesReplayed: 5, AssociationsStrengthened: 3, - NewAssociationsCreated: 1, - NoisyMemoriesDemoted: 0, - DurationMs: 100, - Ts: time.Now(), + NewAssociationsCreated: 1, + NoisyMemoriesDemoted: 0, + DurationMs: 100, + Ts: time.Now(), } if err := bus.Publish(context.Background(), event); err != nil { diff --git a/internal/llm/lmstudio.go b/internal/llm/lmstudio.go index cc30f3f9..5b93df4b 100644 --- a/internal/llm/lmstudio.go +++ b/internal/llm/lmstudio.go @@ -112,7 +112,7 @@ func (p *LMStudioProvider) doWithRetry(req *http.Request) (*http.Response, error // openAIMessage wraps a Message for OpenAI API serialization. type openAIMessage struct { Role string `json:"role"` - Content *string `json:"content"` // pointer: null for tool-call assistant messages + Content *string `json:"content"` // pointer: null for tool-call assistant messages ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` } @@ -142,7 +142,7 @@ type openAIToolFunction struct { // openAIResponseFormat mirrors the OpenAI response_format parameter. type openAIResponseFormat struct { - Type string `json:"type"` // "text", "json_object", or "json_schema" + Type string `json:"type"` // "text", "json_object", or "json_schema" JSONSchema *openAIJSONSchema `json:"json_schema,omitempty"` // for "json_schema" type } @@ -162,7 +162,7 @@ type openAICompletionRequest struct { TopP float32 `json:"top_p,omitempty"` Stop []string `json:"stop,omitempty"` Tools []openAITool `json:"tools,omitempty"` - ResponseFormat *openAIResponseFormat `json:"response_format,omitempty"` + ResponseFormat *openAIResponseFormat `json:"response_format,omitempty"` } // openAIChoice represents a single choice in a completion response. @@ -190,8 +190,8 @@ type openAICompletionResponse struct { // openAIEmbeddingRequest is the request format for the OpenAI-compatible embeddings API. type openAIEmbeddingRequest struct { - Model string `json:"model"` - Input []string `json:"input"` + Model string `json:"model"` + Input []string `json:"input"` } // openAIEmbeddingData represents a single embedding in the response. diff --git a/internal/llm/provider.go b/internal/llm/provider.go index c013f2ab..1a03ae81 100644 --- a/internal/llm/provider.go +++ b/internal/llm/provider.go @@ -8,10 +8,10 @@ import ( // Message represents a single turn in a conversation. type Message struct { - Role string `json:"role"` // "system", "user", "assistant", "tool" + Role string `json:"role"` // "system", "user", "assistant", "tool" Content string `json:"content,omitempty"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` // populated when assistant requests tool use - ToolCallID string `json:"tool_call_id,omitempty"` // set when Role="tool" to match the request + ToolCalls []ToolCall `json:"tool_calls,omitempty"` // populated when assistant requests tool use + ToolCallID string `json:"tool_call_id,omitempty"` // set when Role="tool" to match the request } // ResponseFormat specifies the expected output format for the LLM response. diff --git a/internal/store/sqlite/embindex_test.go b/internal/store/sqlite/embindex_test.go index ad87d17d..b5410b8d 100644 --- a/internal/store/sqlite/embindex_test.go +++ b/internal/store/sqlite/embindex_test.go @@ -207,7 +207,7 @@ func TestEmbeddingIndexSearchDimensionMismatch(t *testing.T) { idx := newEmbeddingIndex() idx.Add("mem-1", []float32{1.0, 0.0, 0.0}) // 3D - idx.Add("mem-2", []float32{1.0, 0.0}) // 2D + idx.Add("mem-2", []float32{1.0, 0.0}) // 2D results := idx.Search([]float32{1.0, 0.0, 0.0}, 10) // 3D query if len(results) != 1 { @@ -223,9 +223,9 @@ func TestEmbeddingIndexSearchSorted(t *testing.T) { // Add vectors at varying angles to the query idx.Add("far", []float32{0.0, 1.0, 0.0}) // orthogonal - idx.Add("close", []float32{0.9, 0.1, 0.0}) // close - idx.Add("identical", []float32{1.0, 0.0, 0.0}) // identical - idx.Add("moderate", []float32{0.5, 0.5, 0.0}) // moderate + idx.Add("close", []float32{0.9, 0.1, 0.0}) // close + idx.Add("identical", []float32{1.0, 0.0, 0.0}) // identical + idx.Add("moderate", []float32{0.5, 0.5, 0.0}) // moderate results := idx.Search([]float32{1.0, 0.0, 0.0}, 4) if len(results) != 4 { diff --git a/internal/store/sqlite/episodes_test.go b/internal/store/sqlite/episodes_test.go index 7c3ae453..b42f97c1 100644 --- a/internal/store/sqlite/episodes_test.go +++ b/internal/store/sqlite/episodes_test.go @@ -17,21 +17,21 @@ func TestCreateAndGetEpisode(t *testing.T) { ctx := context.Background() ep := store.Episode{ - ID: "ep-001", - Title: "Test Episode", - StartTime: time.Now().Add(-30 * time.Minute), - EndTime: time.Now(), - DurationSec: 1800, - RawMemoryIDs: []string{"raw-1", "raw-2"}, - MemoryIDs: []string{}, - Summary: "A test episode", - Narrative: "This is a detailed narrative about the test episode.", - Salience: 0.7, + ID: "ep-001", + Title: "Test Episode", + StartTime: time.Now().Add(-30 * time.Minute), + EndTime: time.Now(), + DurationSec: 1800, + RawMemoryIDs: []string{"raw-1", "raw-2"}, + MemoryIDs: []string{}, + Summary: "A test episode", + Narrative: "This is a detailed narrative about the test episode.", + Salience: 0.7, EmotionalTone: "satisfying", - Outcome: "success", - State: "open", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + Outcome: "success", + State: "open", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } err := s.CreateEpisode(ctx, ep) diff --git a/internal/store/sqlite/patterns.go b/internal/store/sqlite/patterns.go index e48b0f4c..af2f2b89 100644 --- a/internal/store/sqlite/patterns.go +++ b/internal/store/sqlite/patterns.go @@ -271,4 +271,3 @@ func scanPatternRows(rows *sql.Rows) ([]store.Pattern, error) { } // cosineSimilarity and sqrt32 are defined in embindex.go - diff --git a/internal/store/sqlite/resolutions_test.go b/internal/store/sqlite/resolutions_test.go index a89fcdf7..b4b740bf 100644 --- a/internal/store/sqlite/resolutions_test.go +++ b/internal/store/sqlite/resolutions_test.go @@ -82,11 +82,11 @@ func TestWriteAndGetConceptSet(t *testing.T) { } cs := store.ConceptSet{ - MemoryID: "mem-cs-1", - Topics: []store.Topic{{Label: "Go", Path: "programming/go"}}, - Entities: []store.Entity{{Name: "main.go", Type: "file", Context: "modified"}}, - Actions: []store.Action{{Verb: "debugged", Object: "auth system", Details: "fixed token cache"}}, - Causality: []store.CausalLink{{Relation: "caused_by", Description: "token TTL was wrong"}}, + MemoryID: "mem-cs-1", + Topics: []store.Topic{{Label: "Go", Path: "programming/go"}}, + Entities: []store.Entity{{Name: "main.go", Type: "file", Context: "modified"}}, + Actions: []store.Action{{Verb: "debugged", Object: "auth system", Details: "fixed token cache"}}, + Causality: []store.CausalLink{{Relation: "caused_by", Description: "token TTL was wrong"}}, Significance: "important", CreatedAt: time.Now(), } diff --git a/internal/store/sqlite/sqlite.go b/internal/store/sqlite/sqlite.go index dfa75a8f..9ac7d31e 100644 --- a/internal/store/sqlite/sqlite.go +++ b/internal/store/sqlite/sqlite.go @@ -861,7 +861,9 @@ func (s *SQLiteStore) SearchByFullText(ctx context.Context, query string, limit m.salience, m.access_count, m.last_accessed, m.state, m.gist_of, m.episode_id, m.project, m.session_id, m.created_at, m.updated_at FROM memories m - WHERE m.rowid IN (SELECT rowid FROM memories_fts WHERE memories_fts MATCH ?) + JOIN memories_fts ON m.rowid = memories_fts.rowid + WHERE memories_fts MATCH ? + ORDER BY memories_fts.rank LIMIT ? ` diff --git a/internal/watcher/clipboard/watcher.go b/internal/watcher/clipboard/watcher.go index 02a63388..0b9efe87 100644 --- a/internal/watcher/clipboard/watcher.go +++ b/internal/watcher/clipboard/watcher.go @@ -31,8 +31,8 @@ type ClipboardWatcher struct { running bool lastContentHash string lastNContentHashes []string // track last N contents to avoid duplicates - maxHistorySize int // number of previous clips to track - enabled bool // whether clipboard reading is supported on this platform + maxHistorySize int // number of previous clips to track + enabled bool // whether clipboard reading is supported on this platform } // NewClipboardWatcher creates a new clipboard watcher. diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 90a1b7ba..ec3cd1b1 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -8,8 +8,8 @@ import ( // Event represents a raw observation from a watcher. type Event struct { ID string `json:"id"` - Source string `json:"source"` // "filesystem", "terminal", "clipboard" - Type string `json:"type"` // "file_created", "file_modified", "command_executed", "clipboard_changed" + Source string `json:"source"` // "filesystem", "terminal", "clipboard" + Type string `json:"type"` // "file_created", "file_modified", "command_executed", "clipboard_changed" Timestamp time.Time `json:"timestamp"` Path string `json:"path,omitempty"` // for filesystem events Content string `json:"content"`