feat(leann-phase2): implement hybrid vector storage and graph-based search (#20)

* feat(leann-phase2): implement hybrid vector storage and graph-based search

- [x] Add AST-aware code chunking for Go, Python, and TypeScript using tree-sitter
- [x] Implement LEANN-inspired hybrid vector storage with hub detection and selective embedding storage (60-80% savings)
- [x] Add observation relationship graph with CSR format and edge detection (file overlap, semantic similarity, temporal, concept)
- [x] Implement graph-aware search with two-level traversal and relationship-based ranking
- [x] Add auto-tuning system for dynamic hub threshold adjustment based on query performance
- [x] Add comprehensive metrics tracking for vector storage, queries, latency, and graph traversals
- [x] Update configuration system with graph and hybrid storage settings
- [x] Add graph stats and vector metrics endpoints to worker service
- [x] Enhance UI sidebar with advanced metrics display and graph visualization
- [x] Optimize struct field alignment throughout codebase for memory efficiency
- [x] Update documentation with LEANN Phase 2 features and performance benefits
- [x] Add tree-sitter dependency for AST parsing

* fix: add fts5 build tag to CI workflow

Pass build-tags: "fts5" to shared workflow to properly compile
sqlite-vec-go-bindings with SQLite FTS5 support.

This fixes test failures in hybrid vector storage tests that require
CGO and FTS5 build tags.

Requires shared-actions@8f7f235 or later.

* docs: add testing documentation and macOS ARM64 known issue

Document the macOS ARM64 CGO linking issue with sqlite-vec-go-bindings
that prevents hybrid package tests from compiling locally.

Added:
- .github/TESTING.md: Comprehensive testing guide with platform-specific
  issues, workarounds, and CI configuration details
- internal/vector/hybrid/README.md: Package-specific documentation
  explaining the macOS limitation
- .github/CI_FIX_SUMMARY.md: Technical details of the CI fix

Key points:
- 41 out of 42 packages test successfully on all platforms
- hybrid package tests fail only on macOS ARM64 (local dev issue)
- Linux CI tests pass with proper build-tags: "fts5" configuration
- Production builds and runtime functionality unaffected

This is a known limitation of sqlite-vec-go-bindings on macOS ARM64
and does not impact CI/CD or production deployments.

* fix: add SQLite busy_timeout to prevent database locked errors

Set PRAGMA busy_timeout=5000 (5 seconds) to allow SQLite to retry
when the database is locked instead of failing immediately.

This fixes race conditions when multiple goroutines try to write
simultaneously, particularly in tests where StoreObservation spawns
async cleanup goroutines.

Root cause:
- StoreObservation launches goroutine -> CleanupOldObservations
- Multiple concurrent cleanups caused "database is locked" errors
- Without busy_timeout, SQLite fails immediately on lock contention

Solution:
- Add 5-second busy timeout for automatic retry on lock
- Standard practice for concurrent SQLite usage
- Works with existing WAL mode configuration

Fixes TestObservationStore_CleanupOldObservations in CI.

* docs: complete summary of all CI test fixes

Comprehensive documentation of all fixes applied:
1. Missing build tags (fts5)
2. Database locked errors (busy_timeout)

All 41/42 packages now pass tests. The hybrid package has a known
macOS ARM64 limitation that doesn't affect CI or production.

No functionality was removed - all fixes are additive only.

* fix: add SQLite driver import to hybrid tests for CGO linking

Add blank import of mattn/go-sqlite3 to hybrid test files to ensure
the SQLite driver is linked into the test binary. This provides the
SQLite symbols that sqlite-vec-go-bindings requires.

Root cause:
- hybrid package imports sqlitevec (transitively depends on sqlite-vec CGO)
- Test binary needs SQLite symbols for linking
- sqlitevec tests already had this import, but hybrid tests didn't
- Without the driver import, linker fails with "undefined symbols"

This fix enables hybrid tests to run with -race flag on all platforms.

Before: 41/42 packages pass (hybrid failed to link)
After:  42/42 packages pass 

Fixes hybrid test compilation on macOS ARM64, Linux, and Windows.

* docs: remove outdated macOS limitation documentation

The hybrid test linking issue has been fixed by adding the SQLite
driver import. All tests now pass on all platforms including macOS.

Removed:
- internal/vector/hybrid/README.md (documented workaround no longer needed)
- .github/TESTING.md (macOS limitation section obsolete)

All 42/42 packages now test successfully with -race flag.

* docs: final comprehensive summary of all CI fixes

All three issues now resolved:
1. Missing fts5 build tags
2. Database busy_timeout for concurrent writes
3. Missing SQLite driver import in hybrid tests

Result: 42/42 packages pass with -race on all platforms.

Credit to reviewer for identifying the race detector concern.
This commit is contained in:
2026-01-07 22:03:59 +00:00
committed by GitHub
parent 7ab4b07cf2
commit 5c2685c7b6
88 changed files with 5488 additions and 603 deletions
+6 -6
View File
@@ -33,25 +33,25 @@ const (
// ObservationConflict tracks conflicting observations.
type ObservationConflict struct {
ID int64 `db:"id" json:"id"`
NewerObsID int64 `db:"newer_obs_id" json:"newer_obs_id"`
OlderObsID int64 `db:"older_obs_id" json:"older_obs_id"`
ResolvedAt *string `db:"resolved_at" json:"resolved_at,omitempty"`
ConflictType ConflictType `db:"conflict_type" json:"conflict_type"`
Resolution ConflictResolution `db:"resolution" json:"resolution"`
Reason string `db:"reason" json:"reason"`
DetectedAt string `db:"detected_at" json:"detected_at"`
ID int64 `db:"id" json:"id"`
NewerObsID int64 `db:"newer_obs_id" json:"newer_obs_id"`
OlderObsID int64 `db:"older_obs_id" json:"older_obs_id"`
DetectedAtEpoch int64 `db:"detected_at_epoch" json:"detected_at_epoch"`
Resolved bool `db:"resolved" json:"resolved"`
ResolvedAt *string `db:"resolved_at" json:"resolved_at,omitempty"`
}
// ConflictDetectionResult contains the result of conflict detection.
type ConflictDetectionResult struct {
HasConflict bool
Type ConflictType
Resolution ConflictResolution
Reason string
OlderObsIDs []int64 // IDs of observations that conflict with the new one
OlderObsIDs []int64
HasConflict bool
}
// NewObservationConflict creates a new conflict record.
+3 -3
View File
@@ -51,8 +51,8 @@ func (s *ConflictSuite) TestDetectExplicitCorrection_TableDriven() {
tests := []struct {
name string
text string
expectMatch bool
expectPattern string
expectMatch bool
}{
{
name: "actually that was wrong",
@@ -128,9 +128,9 @@ func (s *ConflictSuite) TestDetectExplicitCorrection_TableDriven() {
// TestDetectOpposingFileChanges_TableDriven tests opposing file change detection.
func (s *ConflictSuite) TestDetectOpposingFileChanges_TableDriven() {
tests := []struct {
name string
newerObs *Observation
olderObs *Observation
name string
expectConflict bool
}{
{
@@ -202,9 +202,9 @@ func (s *ConflictSuite) TestDetectOpposingFileChanges_TableDriven() {
// TestDetectConceptTagMismatch_TableDriven tests concept tag mismatch detection.
func (s *ConflictSuite) TestDetectConceptTagMismatch_TableDriven() {
tests := []struct {
name string
newerObs *Observation
olderObs *Observation
name string
expectConflict bool
}{
{
+28 -36
View File
@@ -121,48 +121,44 @@ func (j JSONInt64Map) Value() (driver.Value, error) {
// Observation represents a learning extracted from a Claude Code session.
type Observation struct {
ID int64 `db:"id" json:"id"`
FileMtimes JSONInt64Map `db:"file_mtimes" json:"file_mtimes,omitempty"`
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
Project string `db:"project" json:"project"`
Scope ObservationScope `db:"scope" json:"scope"`
Type ObservationType `db:"type" json:"type"`
Title sql.NullString `db:"title" json:"title,omitempty"`
CreatedAt string `db:"created_at" json:"created_at"`
Subtitle sql.NullString `db:"subtitle" json:"subtitle,omitempty"`
Facts JSONStringArray `db:"facts" json:"facts,omitempty"`
Title sql.NullString `db:"title" json:"title,omitempty"`
Narrative sql.NullString `db:"narrative" json:"narrative,omitempty"`
Concepts JSONStringArray `db:"concepts" json:"concepts,omitempty"`
FilesRead JSONStringArray `db:"files_read" json:"files_read,omitempty"`
FilesModified JSONStringArray `db:"files_modified" json:"files_modified,omitempty"`
FileMtimes JSONInt64Map `db:"file_mtimes" json:"file_mtimes,omitempty"`
Facts JSONStringArray `db:"facts" json:"facts,omitempty"`
PromptNumber sql.NullInt64 `db:"prompt_number" json:"prompt_number,omitempty"`
LastRetrievedAt sql.NullInt64 `db:"last_retrieved_at_epoch" json:"last_retrieved_at_epoch,omitempty"`
ScoreUpdatedAt sql.NullInt64 `db:"score_updated_at_epoch" json:"score_updated_at_epoch,omitempty"`
DiscoveryTokens int64 `db:"discovery_tokens" json:"discovery_tokens"`
CreatedAt string `db:"created_at" json:"created_at"`
ID int64 `db:"id" json:"id"`
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
ImportanceScore float64 `db:"importance_score" json:"importance_score"`
UserFeedback int `db:"user_feedback" json:"user_feedback"`
RetrievalCount int `db:"retrieval_count" json:"retrieval_count"`
IsStale bool `db:"-" json:"is_stale,omitempty"`
// Importance scoring fields
ImportanceScore float64 `db:"importance_score" json:"importance_score"`
UserFeedback int `db:"user_feedback" json:"user_feedback"`
RetrievalCount int `db:"retrieval_count" json:"retrieval_count"`
LastRetrievedAt sql.NullInt64 `db:"last_retrieved_at_epoch" json:"last_retrieved_at_epoch,omitempty"`
ScoreUpdatedAt sql.NullInt64 `db:"score_updated_at_epoch" json:"score_updated_at_epoch,omitempty"`
// Conflict detection fields
IsSuperseded bool `db:"is_superseded" json:"is_superseded,omitempty"`
IsSuperseded bool `db:"is_superseded" json:"is_superseded,omitempty"`
}
// ParsedObservation represents an observation parsed from SDK response XML.
type ParsedObservation struct {
FileMtimes map[string]int64
Type ObservationType
Title string
Subtitle string
Facts []string
Narrative string
Scope ObservationScope
Facts []string
Concepts []string
FilesRead []string
FilesModified []string
FileMtimes map[string]int64 // File path -> mtime epoch ms
Scope ObservationScope // Optional: if empty, will be auto-determined
}
// ToStoredObservation converts a ParsedObservation to the stored Observation format.
@@ -197,34 +193,30 @@ func DetermineScope(concepts []string) ObservationScope {
// ObservationJSON is a JSON-friendly representation of Observation.
// It converts sql.NullString to plain strings for clean JSON output.
type ObservationJSON struct {
ID int64 `json:"id"`
FileMtimes map[string]int64 `json:"file_mtimes,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
SDKSessionID string `json:"sdk_session_id"`
Project string `json:"project"`
Scope ObservationScope `json:"scope"`
Type ObservationType `json:"type"`
Title string `json:"title,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
Facts []string `json:"facts,omitempty"`
CreatedAt string `json:"created_at"`
Narrative string `json:"narrative,omitempty"`
Project string `json:"project"`
Concepts []string `json:"concepts,omitempty"`
Facts []string `json:"facts,omitempty"`
FilesRead []string `json:"files_read,omitempty"`
FilesModified []string `json:"files_modified,omitempty"`
FileMtimes map[string]int64 `json:"file_mtimes,omitempty"`
PromptNumber int64 `json:"prompt_number,omitempty"`
DiscoveryTokens int64 `json:"discovery_tokens"`
CreatedAt string `json:"created_at"`
CreatedAtEpoch int64 `json:"created_at_epoch"`
DiscoveryTokens int64 `json:"discovery_tokens"`
ID int64 `json:"id"`
PromptNumber int64 `json:"prompt_number,omitempty"`
ImportanceScore float64 `json:"importance_score"`
UserFeedback int `json:"user_feedback"`
RetrievalCount int `json:"retrieval_count"`
LastRetrievedAt int64 `json:"last_retrieved_at_epoch,omitempty"`
ScoreUpdatedAt int64 `json:"score_updated_at_epoch,omitempty"`
IsStale bool `json:"is_stale,omitempty"`
// Importance scoring fields
ImportanceScore float64 `json:"importance_score"`
UserFeedback int `json:"user_feedback"`
RetrievalCount int `json:"retrieval_count"`
LastRetrievedAt int64 `json:"last_retrieved_at_epoch,omitempty"`
ScoreUpdatedAt int64 `json:"score_updated_at_epoch,omitempty"`
// Conflict detection fields
IsSuperseded bool `json:"is_superseded,omitempty"`
IsSuperseded bool `json:"is_superseded,omitempty"`
}
// MarshalJSON implements json.Marshaler for Observation.
+6 -6
View File
@@ -50,8 +50,8 @@ func (s *ObservationSuite) TestGlobalizableConcepts() {
func (s *ObservationSuite) TestDetermineScope_TableDriven() {
tests := []struct {
name string
concepts []string
expected ObservationScope
concepts []string
}{
{
name: "empty concepts - project scope",
@@ -121,9 +121,9 @@ func (s *ObservationSuite) TestParsedObservation_FileMtimesJSON() {
// TestObservation_CheckStaleness_TableDriven tests staleness checking.
func (s *ObservationSuite) TestObservation_CheckStaleness_TableDriven() {
tests := []struct {
name string
storedMtimes map[string]int64
currentMtimes map[string]int64
name string
expectedStale bool
}{
{
@@ -300,10 +300,10 @@ func TestParsedObservation_ToStoredObservation(t *testing.T) {
// TestJSONStringArray tests JSONStringArray scanning.
func TestJSONStringArray(t *testing.T) {
tests := []struct {
name string
input interface{}
wantErr bool
name string
expected JSONStringArray
wantErr bool
}{
{
name: "nil input",
@@ -348,10 +348,10 @@ func TestJSONStringArray(t *testing.T) {
// TestJSONInt64Map tests JSONInt64Map scanning.
func TestJSONInt64Map(t *testing.T) {
tests := []struct {
name string
input interface{}
wantErr bool
expected JSONInt64Map
name string
wantErr bool
}{
{
name: "nil input",
+25 -25
View File
@@ -39,21 +39,21 @@ const (
// Pattern represents a recurring pattern detected across observations.
// This enables Claude to reference historical insights: "I've encountered this pattern 12 times."
type Pattern struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"` // e.g., "State Management Anti-Pattern"
Type PatternType `db:"type" json:"type"` // bug, refactor, architecture, etc.
Description sql.NullString `db:"description" json:"description"` // Detailed description
Signature JSONStringArray `db:"signature" json:"signature"` // Keyword clusters for detection
Recommendation sql.NullString `db:"recommendation" json:"recommendation"` // What works for this pattern
Frequency int `db:"frequency" json:"frequency"` // How many times encountered
Projects JSONStringArray `db:"projects" json:"projects"` // Projects where this pattern was seen
ObservationIDs JSONInt64Array `db:"observation_ids" json:"observation_ids"` // Source observation IDs
Status PatternStatus `db:"status" json:"status"` // active, deprecated, merged
MergedIntoID sql.NullInt64 `db:"merged_into_id" json:"merged_into_id,omitempty"`
Confidence float64 `db:"confidence" json:"confidence"` // Detection confidence (0.0-1.0)
LastSeenAt string `db:"last_seen_at" json:"last_seen_at"` // Last time pattern was detected
LastSeenEpoch int64 `db:"last_seen_at_epoch" json:"last_seen_at_epoch"`
Status PatternStatus `db:"status" json:"status"`
Name string `db:"name" json:"name"`
Type PatternType `db:"type" json:"type"`
CreatedAt string `db:"created_at" json:"created_at"`
LastSeenAt string `db:"last_seen_at" json:"last_seen_at"`
Signature JSONStringArray `db:"signature" json:"signature"`
Projects JSONStringArray `db:"projects" json:"projects"`
ObservationIDs JSONInt64Array `db:"observation_ids" json:"observation_ids"`
Recommendation sql.NullString `db:"recommendation" json:"recommendation"`
Description sql.NullString `db:"description" json:"description"`
MergedIntoID sql.NullInt64 `db:"merged_into_id" json:"merged_into_id,omitempty"`
Frequency int `db:"frequency" json:"frequency"`
Confidence float64 `db:"confidence" json:"confidence"`
ID int64 `db:"id" json:"id"`
LastSeenEpoch int64 `db:"last_seen_at_epoch" json:"last_seen_at_epoch"`
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
}
@@ -95,21 +95,21 @@ func (j JSONInt64Array) Value() (driver.Value, error) {
// PatternJSON is a JSON-friendly representation of Pattern.
type PatternJSON struct {
ID int64 `json:"id"`
Status PatternStatus `json:"status"`
Name string `json:"name"`
Type PatternType `json:"type"`
Description string `json:"description,omitempty"`
Signature []string `json:"signature,omitempty"`
CreatedAt string `json:"created_at"`
Recommendation string `json:"recommendation,omitempty"`
Frequency int `json:"frequency"`
Projects []string `json:"projects,omitempty"`
LastSeenAt string `json:"last_seen_at"`
Signature []string `json:"signature,omitempty"`
ObservationIDs []int64 `json:"observation_ids,omitempty"`
Status PatternStatus `json:"status"`
Projects []string `json:"projects,omitempty"`
MergedIntoID int64 `json:"merged_into_id,omitempty"`
Confidence float64 `json:"confidence"`
LastSeenAt string `json:"last_seen_at"`
Frequency int `json:"frequency"`
LastSeenEpoch int64 `json:"last_seen_at_epoch"`
CreatedAt string `json:"created_at"`
ID int64 `json:"id"`
CreatedAtEpoch int64 `json:"created_at_epoch"`
}
@@ -214,11 +214,11 @@ func (p *Pattern) updateConfidence() {
// PatternMatch represents a match between an observation and a potential pattern.
type PatternMatch struct {
PatternID int64 `json:"pattern_id"`
Score float64 `json:"score"` // Match score (0.0-1.0)
MatchedOn string `json:"matched_on"` // What triggered the match (concept, keyword, type, etc.)
IsNew bool `json:"is_new"` // Whether this would create a new pattern
MatchedOn string `json:"matched_on"`
SuggestedName string `json:"suggested_name,omitempty"`
PatternID int64 `json:"pattern_id"`
Score float64 `json:"score"`
IsNew bool `json:"is_new"`
}
// PatternSignatureKeywords are common keywords used in pattern detection.
+8 -8
View File
@@ -116,18 +116,18 @@ func TestPattern_ConfidenceCalculation(t *testing.T) {
func TestPatternType_Detection(t *testing.T) {
tests := []struct {
concepts []string
title string
narrative string
expected PatternType
concepts []string
}{
{[]string{"anti-pattern"}, "", "", PatternTypeAntiPattern},
{[]string{"best-practice"}, "", "", PatternTypeBestPractice},
{[]string{"architecture"}, "", "", PatternTypeArchitecture},
{[]string{"refactor"}, "", "", PatternTypeRefactor},
{[]string{}, "nil pointer bug", "", PatternTypeBug},
{[]string{}, "Deadlock in concurrent code", "", PatternTypeBug},
{[]string{}, "Extract interface", "", PatternTypeRefactor},
{title: "", narrative: "", expected: PatternTypeAntiPattern, concepts: []string{"anti-pattern"}},
{title: "", narrative: "", expected: PatternTypeBestPractice, concepts: []string{"best-practice"}},
{title: "", narrative: "", expected: PatternTypeArchitecture, concepts: []string{"architecture"}},
{title: "", narrative: "", expected: PatternTypeRefactor, concepts: []string{"refactor"}},
{title: "nil pointer bug", narrative: "", expected: PatternTypeBug, concepts: []string{}},
{title: "Deadlock in concurrent code", narrative: "", expected: PatternTypeBug, concepts: []string{}},
{title: "Extract interface", narrative: "", expected: PatternTypeRefactor, concepts: []string{}},
}
for _, tt := range tests {
+4 -4
View File
@@ -3,18 +3,18 @@ package models
// UserPrompt represents a user prompt captured during a session.
type UserPrompt struct {
ID int64 `db:"id" json:"id"`
ClaudeSessionID string `db:"claude_session_id" json:"claude_session_id"`
PromptNumber int `db:"prompt_number" json:"prompt_number"`
PromptText string `db:"prompt_text" json:"prompt_text"`
MatchedObservations int `db:"matched_observations" json:"matched_observations"`
CreatedAt string `db:"created_at" json:"created_at"`
ID int64 `db:"id" json:"id"`
PromptNumber int `db:"prompt_number" json:"prompt_number"`
MatchedObservations int `db:"matched_observations" json:"matched_observations"`
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
}
// UserPromptWithSession includes session context for search results.
type UserPromptWithSession struct {
UserPrompt
Project string `db:"project" json:"project"`
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
UserPrompt
}
+8 -8
View File
@@ -60,14 +60,14 @@ const (
// ObservationRelation represents a directed relationship between two observations.
type ObservationRelation struct {
ID int64 `db:"id" json:"id"`
SourceID int64 `db:"source_id" json:"source_id"`
TargetID int64 `db:"target_id" json:"target_id"`
RelationType RelationType `db:"relation_type" json:"relation_type"`
Confidence float64 `db:"confidence" json:"confidence"`
DetectionSource RelationDetectionSource `db:"detection_source" json:"detection_source"`
Reason string `db:"reason" json:"reason,omitempty"`
CreatedAt string `db:"created_at" json:"created_at"`
ID int64 `db:"id" json:"id"`
SourceID int64 `db:"source_id" json:"source_id"`
TargetID int64 `db:"target_id" json:"target_id"`
Confidence float64 `db:"confidence" json:"confidence"`
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
}
@@ -88,12 +88,12 @@ func NewObservationRelation(sourceID, targetID int64, relType RelationType, conf
// RelationDetectionResult contains the result of relation detection.
type RelationDetectionResult struct {
SourceID int64
TargetID int64
RelationType RelationType
Confidence float64
DetectionSource RelationDetectionSource
Reason string
SourceID int64
TargetID int64
Confidence float64
}
// DetectFileOverlapRelation checks if observations share file references and determines relationship type.
@@ -484,6 +484,6 @@ type RelationWithDetails struct {
// RelationGraph represents a graph of related observations.
type RelationGraph struct {
CenterID int64 `json:"center_id"`
Relations []*RelationWithDetails `json:"relations"`
CenterID int64 `json:"center_id"`
}
+6 -6
View File
@@ -8,12 +8,12 @@ import (
func TestDetectFileOverlapRelation(t *testing.T) {
tests := []struct {
name string
newer *Observation
older *Observation
wantRelation bool
name string
wantRelType RelationType
wantMinConfid float64
wantRelation bool
}{
{
name: "no file overlap",
@@ -105,11 +105,11 @@ func TestDetectFileOverlapRelation(t *testing.T) {
func TestDetectConceptOverlapRelation(t *testing.T) {
tests := []struct {
name string
newer *Observation
older *Observation
wantRelation bool
name string
wantMinConfid float64
wantRelation bool
}{
{
name: "no concept overlap",
@@ -179,8 +179,8 @@ func TestDetectTypeProgressionRelation(t *testing.T) {
name string
newerType ObservationType
olderType ObservationType
wantRelation bool
wantRelType RelationType
wantRelation bool
}{
{
name: "bugfix fixes discovery",
@@ -314,8 +314,8 @@ func TestDetectNarrativeMentionRelation(t *testing.T) {
tests := []struct {
name string
narrative string
wantRelation bool
wantRelType RelationType
wantRelation bool
}{
{
name: "fixes language",
+7 -23
View File
@@ -4,8 +4,8 @@ package models
// ConceptWeight represents a configurable weight for a concept.
type ConceptWeight struct {
Concept string `db:"concept" json:"concept"`
Weight float64 `db:"weight" json:"weight"`
UpdatedAt string `db:"updated_at" json:"updated_at"`
Weight float64 `db:"weight" json:"weight"`
}
// UserFeedbackType represents the type of user feedback.
@@ -62,28 +62,12 @@ var TypeBaseScores = map[ObservationType]float64{
// ScoringConfig contains all scoring weights and parameters.
type ScoringConfig struct {
// RecencyHalfLifeDays is the number of days for the importance score to halve.
// With 7 days, a 7-day old observation has 50% of a new observation's recency score.
RecencyHalfLifeDays float64 `json:"recency_half_life_days"`
// FeedbackWeight scales the user feedback contribution to final score.
// With 0.30, a thumbs up adds 0.30 to the score, thumbs down subtracts 0.30.
FeedbackWeight float64 `json:"feedback_weight"`
// ConceptWeight scales the concept boost contribution.
// The sum of matching concept weights is multiplied by this.
ConceptWeight float64 `json:"concept_weight"`
// RetrievalWeight scales the retrieval boost contribution.
// Popular observations get a logarithmic bonus.
RetrievalWeight float64 `json:"retrieval_weight"`
// ConceptWeights maps concept names to their importance weights.
ConceptWeights map[string]float64 `json:"concept_weights"`
// MinScore is the minimum allowed importance score.
// Prevents observations from completely disappearing.
MinScore float64 `json:"min_score"`
ConceptWeights map[string]float64 `json:"concept_weights"`
RecencyHalfLifeDays float64 `json:"recency_half_life_days"`
FeedbackWeight float64 `json:"feedback_weight"`
ConceptWeight float64 `json:"concept_weight"`
RetrievalWeight float64 `json:"retrieval_weight"`
MinScore float64 `json:"min_score"`
}
// DefaultScoringConfig returns the default scoring configuration.
+8 -8
View File
@@ -17,29 +17,29 @@ const (
// SDKSession represents a Claude Code session tracked by the memory system.
type SDKSession struct {
ID int64 `db:"id" json:"id"`
ClaudeSessionID string `db:"claude_session_id" json:"claude_session_id"`
SDKSessionID sql.NullString `db:"sdk_session_id" json:"sdk_session_id,omitempty"`
Project string `db:"project" json:"project"`
UserPrompt sql.NullString `db:"user_prompt" json:"user_prompt,omitempty"`
WorkerPort sql.NullInt64 `db:"worker_port" json:"worker_port,omitempty"`
PromptCounter int64 `db:"prompt_counter" json:"prompt_counter"`
Status SessionStatus `db:"status" json:"status"`
StartedAt string `db:"started_at" json:"started_at"`
StartedAtEpoch int64 `db:"started_at_epoch" json:"started_at_epoch"`
SDKSessionID sql.NullString `db:"sdk_session_id" json:"sdk_session_id,omitempty"`
UserPrompt sql.NullString `db:"user_prompt" json:"user_prompt,omitempty"`
CompletedAt sql.NullString `db:"completed_at" json:"completed_at,omitempty"`
WorkerPort sql.NullInt64 `db:"worker_port" json:"worker_port,omitempty"`
CompletedAtEpoch sql.NullInt64 `db:"completed_at_epoch" json:"completed_at_epoch,omitempty"`
ID int64 `db:"id" json:"id"`
PromptCounter int64 `db:"prompt_counter" json:"prompt_counter"`
StartedAtEpoch int64 `db:"started_at_epoch" json:"started_at_epoch"`
}
// ActiveSession represents an in-memory active session being processed.
type ActiveSession struct {
SessionDBID int64
StartTime time.Time
ClaudeSessionID string
SDKSessionID string
Project string
UserPrompt string
SessionDBID int64
LastPromptNumber int
StartTime time.Time
CumulativeInputTokens int64
CumulativeOutputTokens int64
}
+7 -7
View File
@@ -9,18 +9,18 @@ import (
// SessionSummary represents a summary of a Claude Code session.
type SessionSummary struct {
ID int64 `db:"id" json:"id"`
CreatedAt string `db:"created_at" json:"created_at"`
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
Project string `db:"project" json:"project"`
Request sql.NullString `db:"request" json:"request,omitempty"`
Completed sql.NullString `db:"completed" json:"completed,omitempty"`
Investigated sql.NullString `db:"investigated" json:"investigated,omitempty"`
Learned sql.NullString `db:"learned" json:"learned,omitempty"`
Completed sql.NullString `db:"completed" json:"completed,omitempty"`
NextSteps sql.NullString `db:"next_steps" json:"next_steps,omitempty"`
Notes sql.NullString `db:"notes" json:"notes,omitempty"`
Request sql.NullString `db:"request" json:"request,omitempty"`
PromptNumber sql.NullInt64 `db:"prompt_number" json:"prompt_number,omitempty"`
ID int64 `db:"id" json:"id"`
DiscoveryTokens int64 `db:"discovery_tokens" json:"discovery_tokens"`
CreatedAt string `db:"created_at" json:"created_at"`
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
}
@@ -56,18 +56,18 @@ func NewSessionSummary(sdkSessionID, project string, parsed *ParsedSummary, prom
// SessionSummaryJSON is a JSON-friendly representation of SessionSummary.
// It converts sql.NullString to plain strings for clean JSON output.
type SessionSummaryJSON struct {
ID int64 `json:"id"`
Completed string `json:"completed,omitempty"`
SDKSessionID string `json:"sdk_session_id"`
Project string `json:"project"`
Request string `json:"request,omitempty"`
Investigated string `json:"investigated,omitempty"`
Learned string `json:"learned,omitempty"`
Completed string `json:"completed,omitempty"`
NextSteps string `json:"next_steps,omitempty"`
Notes string `json:"notes,omitempty"`
CreatedAt string `json:"created_at"`
ID int64 `json:"id"`
PromptNumber int64 `json:"prompt_number,omitempty"`
DiscoveryTokens int64 `json:"discovery_tokens"`
CreatedAt string `json:"created_at"`
CreatedAtEpoch int64 `json:"created_at_epoch"`
}