mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-09 23:59:40 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user