Skip to content

Commit 44f7a80

Browse files
Add TQ-MSE conversation vector search
1 parent 97b4517 commit 44f7a80

13 files changed

Lines changed: 1220 additions & 75 deletions

File tree

‎.agents/skills/sgrep/SKILL.md‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Use `sgrep` for semantic and hybrid search across **code** and **agent conversat
2121
- When ripgrep patterns keep missing relevant code
2222

2323
### Conversation Search
24-
- Finding past discussions with **Claude Code**, **Codex CLI**, or **Cursor**
24+
- Finding past discussions with **Claude Code**, **Codex CLI**, **Cursor**, **OpenCode**, or **Pi**
2525
- Recalling how you solved a similar problem before
2626
- Building context from previous sessions for new tasks
2727
- Searching across all your coding agent interactions
@@ -31,10 +31,16 @@ Use `sgrep` for semantic and hybrid search across **code** and **agent conversat
3131
```bash
3232
# First time only
3333
sgrep setup
34+
sgrep setup --with-rerank # optional, only for --rerank
3435

3536
# Index current directory; builds compact TQ-MSE chunk/file vectors by default
3637
sgrep index .
3738

39+
# Optional ColBERT segment codec override
40+
sgrep index . --colbert-codec tqmse
41+
sgrep index . --colbert-codec int8
42+
sgrep index . --colbert-codec pq6
43+
3844
# Legacy compatibility: also persist full SQL vectors
3945
sgrep index . --sql-vectors
4046

@@ -63,17 +69,32 @@ sgrep --json "rate limiting"
6369
## Conversation Search
6470

6571
```bash
66-
# Index conversations
72+
# Index conversations; refreshes compact TQ-MSE turn vectors
6773
sgrep conv index
74+
sgrep conv index --source codex
75+
sgrep conv index --source claude
76+
sgrep conv index --source opencode
77+
sgrep conv index --source pi
78+
sgrep conv index --watch
79+
sgrep conv index --force
6880

6981
# Search conversations
7082
sgrep conv "authentication flow"
7183
sgrep conv "JWT refresh_token" --hybrid
7284
sgrep conv "database migration" --agent claude --since 7d
85+
sgrep conv "bug fix" --project payment-service --after 2026-01-01 --before 2026-06-01
86+
sgrep conv "exact phrase" --exact
87+
sgrep conv "auth" --json -n 1
7388

74-
# View or export a session
89+
# View, export, context, and copy helpers
7590
sgrep conv view <session_id>
91+
sgrep conv view <session_id> --turn 3 --no-color
92+
sgrep conv export <session_id> --format markdown -o conversation.md
93+
sgrep conv export <session_id> --format json -o conversation.json
7694
sgrep conv context <session_id>
95+
sgrep conv context <session_id> --turns 10 --copy
96+
sgrep conv copy <session_id> --turn 2 --code-only
97+
sgrep conv status
7798
```
7899

79100
## Semantic vs Hybrid

‎README.md‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,13 @@ sgrep setup
7979
sgrep index .
8080

8181
# Optional: override the default TQ-MSE ColBERT codec
82+
sgrep index . --colbert-codec tqmse
8283
sgrep index . --colbert-codec int8
8384
sgrep index . --colbert-codec pq6
8485

86+
# Optional: keep full SQL vectors for legacy vector search
87+
sgrep index . --sql-vectors
88+
8589
# Semantic search (quick)
8690
sgrep "error handling for database connections"
8791

@@ -116,32 +120,43 @@ sgrep conv index --source pi
116120
# Watch mode (auto-index new conversations)
117121
sgrep conv index --watch
118122

123+
# Re-index all cached sessions
124+
sgrep conv index --force
125+
119126
# Search conversations
120127
sgrep conv "authentication"
121128
sgrep conv search "authentication" --agent codex --limit 5
122129
sgrep conv search "session compaction" --agent pi --limit 5
123130
sgrep conv "JWT token" --hybrid
124131
sgrep conv "database migration" --agent claude --since 7d
132+
sgrep conv "auth" --exact
133+
sgrep conv "bug fix" --project payment-service --after 2026-01-01 --before 2026-06-01
125134

126135
# View, export, or resume a session
127136
sgrep conv view <session_id>
137+
sgrep conv view <session_id> --turn 3 --no-color
128138
sgrep conv export <session_id> -o conversation.md
139+
sgrep conv export <session_id> --format json -o conversation.json
129140
sgrep conv resume <session_id>
130141

131142
# Extract context for injection into new session
132143
sgrep conv context <session_id>
144+
sgrep conv context <session_id> --turns 10 --copy
133145

134146
# Copy to clipboard
135147
sgrep conv copy <session_id>
148+
sgrep conv copy <session_id> --turn 2 --code-only
136149

137150
# Check index status
138151
sgrep conv status
139152
```
140153

141154
**Watch mode** monitors conversation directories for all agents and automatically indexes new sessions as they're created. This ensures your conversation search stays up-to-date without manual re-indexing.
142155

143-
Conversations are stored at `~/.sgrep/conversations/conv.db`. Re-running
144-
`sgrep conv index` backfills missing embeddings for existing sessions.
156+
Conversations are stored at `~/.sgrep/conversations/conv.db` with compact
157+
TQ-MSE turn vectors in `~/.sgrep/conversations/turn_embeddings.tqmse`.
158+
Re-running `sgrep conv index` backfills missing embeddings for existing
159+
sessions and refreshes the compact conversation vector sidecar.
145160

146161
Default search output includes the agent session/conversation ID so you can jump
147162
straight to `view`, `resume`, or external agent tooling with the exact session
@@ -237,7 +252,7 @@ sgrep --hybrid --colbert --semantic-weight 0.5 --bm25-weight 0.5 "JWT token"
237252
# Basic setup (embedding model only, ~130MB)
238253
sgrep setup
239254

240-
# With cross-encoder reranking (~1.6GB additional)
255+
# With cross-encoder reranking (~636MB additional)
241256
sgrep setup --with-rerank
242257
```
243258

@@ -324,7 +339,8 @@ All data is stored in `~/.sgrep/`:
324339
│ └── d4e5f6/ # Hash of /path/to/repo2
325340
│ └── ...
326341
├── conversations/
327-
│ └── conv.db # Conversation search index
342+
│ ├── conv.db # Conversation metadata, embeddings, and FTS
343+
│ └── turn_embeddings.tqmse # Compact TQ-MSE conversation turn vectors
328344
├── server.pid # Embedding server PID
329345
└── server.log # Embedding server logs
330346
```
@@ -361,10 +377,19 @@ when you want to re-compact the index after a long watch session.
361377
|---------|-------------|
362378
| `sgrep [query]` | Semantic search (default) |
363379
| `sgrep index [path]` | Index a directory |
380+
| `sgrep index [path] --colbert-codec tqmse/int8/pq6` | Select ColBERT segment codec |
381+
| `sgrep index [path] --sql-vectors` | Also store full SQL vectors for legacy search |
364382
| `sgrep watch [path]` | Watch and auto-index |
365383
| `sgrep list` | List all indexed repos |
366384
| `sgrep status` | Show index status |
367385
| `sgrep clear` | Clear index |
386+
| `sgrep conv [search] <query>` | Search indexed agent conversations |
387+
| `sgrep conv index [--source agent] [--watch] [--force]` | Index or watch conversations |
388+
| `sgrep conv view <session_id> [--turn N] [--no-color]` | View a conversation |
389+
| `sgrep conv export <session_id> --format markdown/json/html` | Export a conversation |
390+
| `sgrep conv context <session_id> [--turns N] [--copy]` | Extract context for a new session |
391+
| `sgrep conv copy <session_id> [--turn N] [--code-only]` | Copy a conversation or turn |
392+
| `sgrep conv status` | Show conversation index status |
368393
| `sgrep setup` | Download embedding model, verify llama-server |
369394
| `sgrep setup --with-rerank` | Also download reranker model (~636MB) |
370395
| `sgrep server start` | Manually start embedding server |
@@ -420,7 +445,7 @@ This creates:
420445
| `-c, --context` | Show code context |
421446
| `--json` | JSON output for agents |
422447
| `-q, --quiet` | Minimal output (paths only) |
423-
| `--threshold F` | L2 distance threshold (default: 1.5, lower = stricter) |
448+
| `--threshold F` | Cosine distance threshold (default: 1.5, lower = stricter) |
424449
| `-t, --include-tests` | Include test files in results (excluded by default) |
425450
| `--all-chunks` | Show all matching chunks (disable deduplication) |
426451
| `--hybrid` | Enable hybrid search (semantic + BM25) |
@@ -440,6 +465,8 @@ SGREP_PORT=8080 # Embedding server port
440465
SGREP_DIMS=768 # Vector dimensions
441466
SGREP_VECTOR_BACKEND=tqmse # Force TQ-MSE search
442467
SGREP_VECTOR_BACKEND=libsql # Force legacy SQL vector search (requires --sql-vectors index)
468+
SGREP_CONV_VECTOR_BACKEND=tqmse # Force TQ-MSE conversation vector search
469+
SGREP_CONV_VECTOR_BACKEND=sqlite # Force legacy SQL conversation vector scan
443470
```
444471
445472
## How It Works

‎internal/cli/conv.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@ func runConvIndex(cmd *cobra.Command, args []string) error {
572572
}
573573
}
574574

575+
vecCount, err := indexer.RebuildTQVectorStore(ctx)
576+
if err != nil {
577+
return fmt.Errorf("refresh compact TQ-MSE conversation vectors: %w", err)
578+
}
579+
fmt.Printf("Refreshed compact TQ-MSE conversation vectors (%d turns)\n", vecCount)
575580
fmt.Printf("\nTotal: %d sessions, %d turns indexed\n", totalSessions, totalTurns)
576581
return nil
577582
}

‎internal/cli/conv_watch.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,12 @@ func indexConversationsOnce(ctx context.Context, indexer *conv.Indexer, parsers
177177
totalTurns += result.TurnsIndexed
178178
}
179179
}
180+
vecCount, err := indexer.RebuildTQVectorStore(ctx)
181+
if err != nil {
182+
return fmt.Errorf("refresh compact TQ-MSE conversation vectors: %w", err)
183+
}
180184
if verbose {
185+
fmt.Printf("Refreshed compact TQ-MSE conversation vectors (%d turns)\n", vecCount)
181186
fmt.Printf("Initial index complete: %d sessions, %d turns\n", totalSessions, totalTurns)
182187
}
183188
return nil
@@ -194,6 +199,7 @@ func processConvJobs(ctx context.Context, indexer *conv.Indexer, pending map[str
194199
}
195200
mu.Unlock()
196201

202+
var indexedTurns int
197203
for _, job := range jobs {
198204
sessions, err := job.parser.Parse(job.path)
199205
if err != nil {
@@ -221,6 +227,19 @@ func processConvJobs(ctx context.Context, indexer *conv.Indexer, pending map[str
221227
fmt.Printf("Indexed %d sessions (%d turns) from %s\n",
222228
result.SessionsIndexed, result.TurnsIndexed, job.path)
223229
}
230+
indexedTurns += result.TurnsIndexed
231+
}
232+
if indexedTurns > 0 {
233+
vecCount, err := indexer.RebuildTQVectorStore(ctx)
234+
if err != nil {
235+
if verbose {
236+
fmt.Fprintf(os.Stderr, "Warning: refresh compact TQ-MSE conversation vectors failed: %v\n", err)
237+
}
238+
return
239+
}
240+
if verbose {
241+
fmt.Printf("Refreshed compact TQ-MSE conversation vectors (%d turns)\n", vecCount)
242+
}
224243
}
225244
}
226245

‎pkg/conv/indexer.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ func (idx *Indexer) IndexSession(ctx context.Context, session *Session) error {
119119
return idx.indexSession(ctx, session)
120120
}
121121

122+
// RebuildTQVectorStore refreshes the compact TQ-MSE sidecar for conversation search.
123+
func (idx *Indexer) RebuildTQVectorStore(ctx context.Context) (int, error) {
124+
return idx.store.RebuildTQVectorStore(ctx)
125+
}
126+
122127
// indexSession indexes a single session and its turns.
123128
func (idx *Indexer) indexSession(ctx context.Context, session *Session) error {
124129
// Estimate tokens

‎pkg/conv/search.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ func (s *Searcher) Search(ctx context.Context, query string, opts SearchOptions)
4646
return nil, fmt.Errorf("failed to generate query embedding: %w", err)
4747
}
4848

49-
if opts.UseHybrid {
49+
hasFilters := opts.Agent != AgentAll || opts.Project != "" || !opts.Since.IsZero() || !opts.Before.IsZero()
50+
if opts.UseHybrid && hasFilters {
51+
results, err = s.store.FilteredHybridSearch(ctx, queryEmb, searchterms.ExtractHybridSearchTerms(query), opts)
52+
} else if opts.UseHybrid {
5053
results, err = s.store.HybridSearch(ctx, queryEmb, searchterms.ExtractHybridSearchTerms(query), opts.Limit*2, opts.Threshold, opts.SemanticWeight, opts.BM25Weight)
51-
} else if opts.Agent != AgentAll || opts.Project != "" || !opts.Since.IsZero() || !opts.Before.IsZero() {
54+
} else if hasFilters {
5255
results, err = s.store.FilteredSearch(ctx, queryEmb, opts)
5356
} else {
5457
results, err = s.store.VectorSearch(ctx, queryEmb, opts.Limit*2, opts.Threshold)

‎pkg/conv/search_test.go‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,75 @@ func TestSearcherExactUsesKeywordSearchWithoutEmbedder(t *testing.T) {
188188
}
189189
}
190190

191+
func TestFilteredHybridSearchAppliesAgentFilter(t *testing.T) {
192+
tmpDir := t.TempDir()
193+
store, err := NewStore(StoreConfig{DBPath: filepath.Join(tmpDir, "test.db"), Dims: defaultDims})
194+
if err != nil {
195+
t.Fatalf("failed to create store: %v", err)
196+
}
197+
defer func() { _ = store.Close() }()
198+
199+
ctx := context.Background()
200+
now := time.Now()
201+
sessions := []*Session{
202+
{
203+
ID: "claude-auth",
204+
Agent: AgentClaudeCode,
205+
ProjectPath: "/repo/auth",
206+
ProjectName: "auth",
207+
StartedAt: now,
208+
EndedAt: now,
209+
Turns: []Turn{
210+
{Index: 0, UserContent: "authentication jwt", AssistContent: "claude answer"},
211+
},
212+
},
213+
{
214+
ID: "codex-auth",
215+
Agent: AgentCodexCLI,
216+
ProjectPath: "/repo/auth",
217+
ProjectName: "auth",
218+
StartedAt: now,
219+
EndedAt: now,
220+
Turns: []Turn{
221+
{Index: 0, UserContent: "authentication jwt", AssistContent: "codex answer"},
222+
},
223+
},
224+
}
225+
for _, session := range sessions {
226+
if err := store.StoreSession(ctx, session); err != nil {
227+
t.Fatalf("failed to store session: %v", err)
228+
}
229+
}
230+
231+
embedding := make([]float32, defaultDims)
232+
embedding[0] = 1
233+
if err := store.StoreTurnEmbeddingBatch(ctx,
234+
[]string{"claude-auth:0", "codex-auth:0"},
235+
[][]float32{embedding, embedding},
236+
); err != nil {
237+
t.Fatalf("failed to store embeddings: %v", err)
238+
}
239+
240+
opts := DefaultSearchOptions()
241+
opts.Agent = AgentCodexCLI
242+
opts.Limit = 10
243+
opts.Threshold = 0
244+
245+
results, err := store.FilteredHybridSearch(ctx, embedding, "authentication", opts)
246+
if err != nil {
247+
t.Fatalf("hybrid search failed: %v", err)
248+
}
249+
if len(results) != 1 {
250+
t.Fatalf("expected 1 filtered result, got %d", len(results))
251+
}
252+
if results[0].SessionID != "codex-auth" {
253+
t.Fatalf("expected codex-auth, got %s", results[0].SessionID)
254+
}
255+
if results[0].MatchType != "hybrid" {
256+
t.Fatalf("expected hybrid match type, got %q", results[0].MatchType)
257+
}
258+
}
259+
191260
func TestConversationHit_Fields(t *testing.T) {
192261
hit := ConversationHit{
193262
SessionID: "test-session",

0 commit comments

Comments
 (0)