mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-13 02:06:24 +00:00
7a061c85eb
* refactor(hooks): simplify hook execution with shared context - [x] Extract BaseInput struct to eliminate duplicate fields across hooks - [x] Create RunHook handler pattern for session-start and user-prompt - [x] Create RunStatuslineHook for fast statusline rendering without worker startup - [x] Add HookContext struct to pass port, project, CWD, SessionID to handlers - [x] Add db/interface.go with ObservationReader/Writer interfaces - [x] Add comprehensive conflict management tests in sqlite/conflict_test.go - [x] Add vector client tests for Count, ModelVersion, NeedsRebuild, GetStaleVectors - [x] Add FilterByThreshold helper tests for query result filtering - [x] Make handlers_test more robust for network-dependent update checks - [x] Update package versions in UI * Move to GORM + general cleanup * feat(mcp): add observation relations discovery and scoring integration - [x] Add find_related_observations MCP tool for discovering related observations by confidence - [x] Integrate scoring calculator and recalculator into MCP server initialization - [x] Add pattern, relation, and session stores to MCP server dependencies - [x] Register MCP server in Claude Code settings during plugin installation - [x] Update install scripts (bash, PowerShell) to configure MCP server settings - [x] Switch plugin manifest files to template-based versioning (plugin.json.tpl, marketplace.json.tpl) - [x] Update all MCP server tests to pass new dependency parameters
187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
// Package scoring provides importance score calculation for observations.
|
|
package scoring
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/internal/db/gorm"
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// ObservationStore defines the interface for observation storage operations needed by the recalculator.
|
|
type ObservationStore interface {
|
|
GetObservationsNeedingScoreUpdate(ctx context.Context, threshold time.Duration, limit int) ([]*models.Observation, error)
|
|
UpdateImportanceScores(ctx context.Context, scores map[int64]float64) error
|
|
GetConceptWeights(ctx context.Context) (map[string]float64, error)
|
|
}
|
|
|
|
// Recalculator periodically recalculates importance scores for observations.
|
|
type Recalculator struct {
|
|
store ObservationStore
|
|
calculator *Calculator
|
|
log zerolog.Logger
|
|
interval time.Duration
|
|
batchSize int
|
|
stopCh chan struct{}
|
|
doneCh chan struct{}
|
|
mu sync.Mutex
|
|
running bool
|
|
}
|
|
|
|
// NewRecalculator creates a new background recalculator.
|
|
func NewRecalculator(store ObservationStore, calc *Calculator, log zerolog.Logger) *Recalculator {
|
|
return &Recalculator{
|
|
store: store,
|
|
calculator: calc,
|
|
log: log.With().Str("component", "recalculator").Logger(),
|
|
interval: 1 * time.Hour, // Run every hour
|
|
batchSize: 500, // Process 500 observations at a time
|
|
stopCh: make(chan struct{}),
|
|
doneCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start begins the background recalculation loop.
|
|
// This should be called in a goroutine.
|
|
func (r *Recalculator) Start(ctx context.Context) {
|
|
r.mu.Lock()
|
|
if r.running {
|
|
r.mu.Unlock()
|
|
return
|
|
}
|
|
r.running = true
|
|
r.mu.Unlock()
|
|
|
|
defer func() {
|
|
r.mu.Lock()
|
|
r.running = false
|
|
r.mu.Unlock()
|
|
close(r.doneCh)
|
|
}()
|
|
|
|
// Initial run
|
|
r.recalculate(ctx)
|
|
|
|
r.mu.Lock()
|
|
interval := r.interval
|
|
r.mu.Unlock()
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.log.Info().Msg("recalculator shutting down due to context cancellation")
|
|
return
|
|
case <-r.stopCh:
|
|
r.log.Info().Msg("recalculator stopping")
|
|
return
|
|
case <-ticker.C:
|
|
r.recalculate(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the background recalculation loop.
|
|
func (r *Recalculator) Stop() {
|
|
r.mu.Lock()
|
|
if !r.running {
|
|
r.mu.Unlock()
|
|
return
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
close(r.stopCh)
|
|
<-r.doneCh
|
|
}
|
|
|
|
// recalculate performs a single recalculation batch.
|
|
func (r *Recalculator) recalculate(ctx context.Context) {
|
|
now := time.Now()
|
|
threshold := r.calculator.RecalculateThreshold()
|
|
|
|
r.mu.Lock()
|
|
batchSize := r.batchSize
|
|
r.mu.Unlock()
|
|
|
|
observations, err := r.store.GetObservationsNeedingScoreUpdate(ctx, threshold, batchSize)
|
|
if err != nil {
|
|
r.log.Error().Err(err).Msg("failed to get observations for score update")
|
|
return
|
|
}
|
|
|
|
if len(observations) == 0 {
|
|
return
|
|
}
|
|
|
|
scores := r.calculator.BatchCalculate(observations, now)
|
|
|
|
if err := r.store.UpdateImportanceScores(ctx, scores); err != nil {
|
|
r.log.Error().Err(err).Msg("failed to update importance scores")
|
|
return
|
|
}
|
|
|
|
r.log.Info().
|
|
Int("count", len(scores)).
|
|
Dur("elapsed", time.Since(now)).
|
|
Msg("recalculated importance scores")
|
|
}
|
|
|
|
// RecalculateNow triggers an immediate recalculation.
|
|
// This is useful for testing or when scores need to be updated urgently.
|
|
func (r *Recalculator) RecalculateNow(ctx context.Context) error {
|
|
r.recalculate(ctx)
|
|
return nil
|
|
}
|
|
|
|
// RefreshConceptWeights reloads concept weights from the database.
|
|
// Call this after updating concept weights to apply changes.
|
|
func (r *Recalculator) RefreshConceptWeights(ctx context.Context) error {
|
|
weights, err := r.store.GetConceptWeights(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config := r.calculator.GetConfig()
|
|
config.ConceptWeights = weights
|
|
r.calculator.UpdateConfig(config)
|
|
|
|
r.log.Info().Int("count", len(weights)).Msg("refreshed concept weights")
|
|
return nil
|
|
}
|
|
|
|
// Stats returns statistics about the recalculator.
|
|
type Stats struct {
|
|
Running bool `json:"running"`
|
|
Interval time.Duration `json:"interval"`
|
|
BatchSize int `json:"batch_size"`
|
|
HalfLife float64 `json:"half_life_days"`
|
|
MinScore float64 `json:"min_score"`
|
|
ConceptsLen int `json:"concepts_count"`
|
|
}
|
|
|
|
// GetStats returns current recalculator statistics.
|
|
func (r *Recalculator) GetStats() Stats {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
config := r.calculator.GetConfig()
|
|
|
|
return Stats{
|
|
Running: r.running,
|
|
Interval: r.interval,
|
|
BatchSize: r.batchSize,
|
|
HalfLife: config.RecencyHalfLifeDays,
|
|
MinScore: config.MinScore,
|
|
ConceptsLen: len(config.ConceptWeights),
|
|
}
|
|
}
|
|
|
|
// Ensure ObservationStore satisfies the interface
|
|
var _ ObservationStore = (*gorm.ObservationStore)(nil)
|