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 .knowns/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"dimensions": 384,
"maxTokens": 512
},
"serverPort": 4455,
"serverPort": 6420,
"platforms": [
"claude-code",
"opencode",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: 'Learning: Code Ingest Performance Optimization'
description: Learnings from optimizing code ingest pipeline — 5x speedup via batch embedding, content compaction, and parser pooling
createdAt: '2026-04-30T13:00:36.691Z'
updatedAt: '2026-04-30T13:00:36.691Z'
tags:
- learning
- performance
- embedding
- ast
- code-intelligence
---

# Learning: Code Ingest Performance Optimization

## Patterns

### Batch Embedding with Length-Sorted Content
- **What:** Sort code symbols by embedding content length before batching ORT inference. Short symbols batch together (minimal padding), long symbols batch separately.
- **When to use:** Any ORT/ONNX batch inference where inputs vary widely in length.
- **Impact:** 5x wall-clock speedup (1m27s → 17s on 3700+ symbols).
- **Why it works:** ORT pads all texts in a batch to the longest one. Without sorting, one long function in a batch of 64 forces all 63 short symbols to be padded to max length → wasted compute.

### Compact Embedding Content for Code Symbols
- **What:** Use signature + edge summary instead of full source code for embedding content. Functions/methods get signature only. Classes get member signature list.
- **When to use:** Code symbol embedding where the model has limited token budget (512 tokens).
- **Why:** Full source code gets truncated by the model anyway. Signature + relationships (calls, imports, instantiates) capture the semantic meaning better for search.

### Parser Instance Pooling
- **What:** Pool tree-sitter parser instances per language extension using a bounded stack. Avoids alloc/dealloc per file.
- **When to use:** Sequential file parsing with tree-sitter CGO bindings.
- **Impact:** Minor but measurable — eliminates per-file parser creation overhead.

## Decisions

### Sort + Adaptive Batch Size
- **Chose:** Sort by content length + adaptive batch size (64 for short, 32 for long)
- **Over:** Fixed batch size, parallel embedding goroutines
- **Tag:** GOOD_CALL
- **Outcome:** Simple change, massive impact. No concurrency complexity.
- **Recommendation:** Always sort by input length before batching any padded inference.

### Add Java/Rust/C# on CGO Path Now
- **Chose:** Add languages to current Go CGO tree-sitter path
- **Over:** Wait for sidecar migration to complete
- **Tag:** TRADEOFF
- **Outcome:** Users get new language support immediately. Will need to port to sidecar later but the AST node type mapping is reusable.

### Truncate Content at 2000 Chars
- **Chose:** Hard truncate embedding content at 2000 chars before tokenization
- **Over:** Let tokenizer handle full text then truncate at token level
- **Tag:** GOOD_CALL
- **Outcome:** Reduces tokenizer CPU overhead significantly. Model uses max 512 tokens ≈ 2000 chars for code.

## Failures

### WebUI Memory Page Silent Failure
- **What went wrong:** Memory page showed "0 entries" despite API returning 77 entries.
- **Root cause:** Frontend `Promise.all([memoryApi.list(), workingMemoryApi.list()])` — the `/api/working-memories` route was removed from backend but frontend still called it → 404 → `Promise.all` rejects → catch block runs → both persistent and working entries stay as empty arrays.
- **Time lost:** ~20 minutes debugging.
- **Prevention:** When removing a backend route, always grep frontend for the route path. Or use `Promise.allSettled` instead of `Promise.all` for independent fetches.

### Duplicate "Semantic search ready" Message
- **What went wrong:** `knowns sync` printed "✓ Semantic search ready (model: ...)" twice.
- **Root cause:** `ensureProjectAndGlobalSemanticReady` calls `ensureSemanticStoreReady` twice (project + global), each calling `runSemanticSetup` which printed the message independently.
- **Prevention:** Print status messages at the orchestration level, not inside reusable helper functions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: 'Learning: MCP Setup Multi-Platform Registry'
description: Learnings from rewriting knowns mcp setup to support per-platform selection and 10 platforms
createdAt: '2026-04-30T12:58:14.440Z'
updatedAt: '2026-04-30T12:58:14.440Z'
tags:
- learning
- cli
- mcp
- platforms
---

# Learning: MCP Setup Multi-Platform Registry

Source: `internal/cli/mcp.go` rewrite (April 2026)

## Patterns

### Platform Registry Pattern
- **What:** Data-driven registry of platform configs using a slice of structs with `id`, `label`, `scope`, and `setup func`. CLI args map to registry entries, replacing hardcoded if/else branches.
- **When to use:** Any CLI command that needs to operate across multiple targets (platforms, runtimes, environments). Especially useful when the list of targets grows over time.
- **Example:** `mcpPlatforms` slice in `internal/cli/mcp.go` — each entry is self-contained with its own setup function, making it trivial to add new platforms.

### Unified Setup Function Signature
- **What:** All platform setup functions normalized to `func(projectRoot string) (path string, err error)`. Previous code had inconsistent signatures.
- **When to use:** When building a registry of operations that need uniform dispatch.

## Decisions

### Reuse init.go config functions
- **Chose:** Delegate to existing `create*Quiet` functions from `init.go`
- **Over:** Duplicating config creation logic in `mcp.go`
- **Tag:** GOOD_CALL
- **Outcome:** Zero duplication, behavior stays consistent between `knowns init` and `knowns mcp setup`
- **Recommendation:** Always check `init.go` and `sync.go` before writing new platform config logic — the function likely already exists.

### Positional args over per-platform flags
- **Chose:** `knowns mcp setup claude kiro antigravity` (positional args)
- **Over:** `knowns mcp setup --claude --kiro --antigravity` (boolean flags)
- **Tag:** GOOD_CALL
- **Outcome:** Cleaner UX, shell completion works naturally, scales to any number of platforms without flag explosion.
- **Recommendation:** Use positional args for target selection when the set is open-ended. Use flags for behavioral modifiers (`--project`, `--global`).

### Claude Desktop path fix (macOS)
- **Chose:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Over:** `~/.claude/claude_desktop_config.json` (old, incorrect path)
- **Tag:** TRADEOFF
- **Outcome:** Correct for Claude Desktop app. The old path was the Claude Code CLI config, not Claude Desktop.
- **Recommendation:** Always verify platform config paths against official docs before hardcoding.

## Failures

No significant failures. The implementation was straightforward because most platform config functions already existed in `init.go`. The main gap was that the original `mcp setup` was a minimal stub that only wrote one global config."
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/rs/cors v1.11.1
github.com/spf13/cobra v1.10.2
github.com/yalue/onnxruntime_go v1.27.0
github.com/tree-sitter/go-tree-sitter v0.25.0
github.com/tree-sitter/tree-sitter-go v0.25.0
github.com/tree-sitter/tree-sitter-javascript v0.25.0
github.com/tree-sitter/tree-sitter-python v0.25.0
github.com/tree-sitter/tree-sitter-typescript v0.23.2
github.com/yalue/onnxruntime_go v1.27.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.1
)
Expand All @@ -28,6 +28,9 @@ require (
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 // indirect
github.com/tree-sitter/tree-sitter-java v0.23.5 // indirect
github.com/tree-sitter/tree-sitter-rust v0.24.2 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ github.com/tree-sitter/go-tree-sitter v0.25.0 h1:sx6kcg8raRFCvc9BnXglke6axya12kr
github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU=
github.com/tree-sitter/tree-sitter-c v0.23.4 h1:nBPH3FV07DzAD7p0GfNvXM+Y7pNIoPenQWBpvM++t4c=
github.com/tree-sitter/tree-sitter-c v0.23.4/go.mod h1:MkI5dOiIpeN94LNjeCp8ljXN/953JCwAby4bClMr6bw=
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 h1:EeUc2WJE5G1pD6YEqera2yVHYeroeR+/MakKX2a+0xQ=
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5/go.mod h1:H7/aFm5vR1A8Yn5VIOfLWPdlKuJsMgZ5eDmaJdv8bY0=
github.com/tree-sitter/tree-sitter-cpp v0.23.4 h1:LaWZsiqQKvR65yHgKmnaqA+uz6tlDJTJFCyFIeZU/8w=
github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8=
github.com/tree-sitter/tree-sitter-embedded-template v0.23.2 h1:nFkkH6Sbe56EXLmZBqHHcamTpmz3TId97I16EnGy4rg=
Expand All @@ -163,6 +165,8 @@ github.com/tree-sitter/tree-sitter-ruby v0.23.1 h1:T/NKHUA+iVbHM440hFx+lzVOzS4dV
github.com/tree-sitter/tree-sitter-ruby v0.23.1/go.mod h1:kUS4kCCQloFcdX6sdpr8p6r2rogbM6ZjTox5ZOQy8cA=
github.com/tree-sitter/tree-sitter-rust v0.23.2 h1:6AtoooCW5GqNrRpfnvl0iUhxTAZEovEmLKDbyHlfw90=
github.com/tree-sitter/tree-sitter-rust v0.23.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI=
github.com/tree-sitter/tree-sitter-rust v0.24.2 h1:NL4nF67ib21RMzzfvkmXlVwe45vvhW10DVyO+D0z/W0=
github.com/tree-sitter/tree-sitter-rust v0.24.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI=
github.com/tree-sitter/tree-sitter-typescript v0.23.2 h1:/Odvphn18PniVixb9e97X0DbNVsU6Qocv9mfkyzdXwU=
github.com/tree-sitter/tree-sitter-typescript v0.23.2/go.mod h1:zjzMXT/Ulffel2xfOcAkQQkiAkmgnbtPGlFQw/5X4xA=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/download_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,6 @@ func runSemanticSetup(modelID string, force ...bool) error {
}

if !forceDownload && isModelInstalled(selected) {
fmt.Println(StyleSuccess.Render(fmt.Sprintf("✓ Semantic search ready (model: %s)", modelID)))
return nil
}

Expand Down Expand Up @@ -500,5 +499,7 @@ func ensureProjectAndGlobalSemanticReady(projectStore *storage.Store, defaultMod
if err != nil {
return projectChanged, false, err
}
// Print ready message once after both stores are set up.
fmt.Println(StyleSuccess.Render(fmt.Sprintf("✓ Semantic search ready (model: %s)", defaultModelID)))
return projectChanged, globalChanged, nil
}
76 changes: 69 additions & 7 deletions internal/cli/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"

Expand All @@ -22,7 +23,8 @@ var ingestCmd = &cobra.Command{
Long: `Index all code files in the project using tree-sitter AST parsing.

This command walks the project directory, parses AST for Go, TypeScript,
JavaScript, and Python files, and stores code symbols as indexed code data.
JavaScript, Python, Java, Rust, and C# files, and stores code symbols as
indexed code data.

Files matching .gitignore and test files (*_test.go, *.spec.ts, etc.)
are skipped by default. Use --include-tests to include them.
Expand Down Expand Up @@ -240,16 +242,76 @@ func runIngestWithProgress(projectRoot string, includeTests bool) (symCount, fil

vecStore.RemoveByPrefix("code::")

var chunks []search.Chunk
// Pre-compute chunks and sort by content length to minimize padding
// waste in ORT batch inference (all texts in a batch are padded to
// the longest one).
type indexedChunk struct {
idx int
chunk search.Chunk
}
preChunks := make([]indexedChunk, 0, len(syms))
for i, sym := range syms {
chunk := sym.ToChunk()
vec, err := embedder.EmbedDocument(chunk.Content)
preChunks = append(preChunks, indexedChunk{idx: i, chunk: sym.ToChunk()})
}

// Truncate embedding content to ~2000 chars. The model uses max 512
// tokens (~2000 chars for code). Sending longer text only wastes
// tokenizer CPU without improving the embedding vector.
const maxEmbedChars = 2000
for i := range preChunks {
if len(preChunks[i].chunk.Content) > maxEmbedChars {
preChunks[i].chunk.Content = preChunks[i].chunk.Content[:maxEmbedChars]
}
}

sort.Slice(preChunks, func(a, b int) bool {
return len(preChunks[a].chunk.Content) < len(preChunks[b].chunk.Content)
})

// Batch embed for much better throughput.
// Use adaptive batch size: larger batches for short content,
// smaller for long content to manage memory.
var chunks []search.Chunk
i := 0
for i < len(preChunks) {
// Adaptive batch size based on content length of first item.
batchSize := 64
if len(preChunks[i].chunk.Content) > 1000 {
batchSize = 32
}
end := i + batchSize
if end > len(preChunks) {
end = len(preChunks)
}
batch := preChunks[i:end]

texts := make([]string, len(batch))
for j, ic := range batch {
texts[j] = ic.chunk.Content
}

vecs, err := embedder.EmbedDocumentBatch(texts)
if err != nil {
// Fallback: embed one-by-one on batch failure
for j, ic := range batch {
vec, err := embedder.EmbedDocument(ic.chunk.Content)
if err != nil {
continue
}
ic.chunk.Embedding = vec
chunks = append(chunks, ic.chunk)
state.processed = i + j + 1
}
i = end
continue
}
chunk.Embedding = vec
chunks = append(chunks, chunk)
state.processed = i + 1

for j := range batch {
batch[j].chunk.Embedding = vecs[j]
chunks = append(chunks, batch[j].chunk)
}
state.processed = end
i = end
}

vecStore.AddChunks(chunks)
Expand Down
Loading
Loading