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
2 changes: 1 addition & 1 deletion .claude/hooks/protect-git.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions cmd/mnemonic/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 24 additions & 14 deletions internal/agent/consolidation/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/agent/consolidation/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 15 additions & 6 deletions internal/agent/dreaming/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}

Expand Down Expand Up @@ -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.
Expand Down
50 changes: 25 additions & 25 deletions internal/agent/encoding/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,18 +80,18 @@ 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"`
Salience float32 `json:"salience"`
}

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 {
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 11 additions & 11 deletions internal/agent/metacognition/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
}
}
Expand Down Expand Up @@ -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,
},
}
}
Expand Down Expand Up @@ -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(),
}
Expand Down
42 changes: 21 additions & 21 deletions internal/agent/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
Loading