diff --git a/README.md b/README.md
index 4af5ba5..7eb9bba 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ Grep and fuzzy file search work fine for small projects. At scale they break dow
│ └── embedded Swagger UI │
│ │
│ Indexing pipeline │
-│ ├── gotreesitter (AST chunking, 200+ languages) │
+│ ├── tree-sitter/wasm (AST chunking, 31 langs) (wazero) │
│ ├── llama-server sidecar (Unix socket → CodeRankEmbed Q8 GGUF) │
│ ├── chromem-go (cosine similarity vector store) │
│ ├── SQLite FTS5 chunk mirror (BM25 — powers hybrid workspace) │
diff --git a/doc/openapi.yaml b/doc/openapi.yaml
index 4b2afa9..a3acf20 100644
--- a/doc/openapi.yaml
+++ b/doc/openapi.yaml
@@ -3245,6 +3245,8 @@ components:
- max_embedding_concurrency
- llama_batch_size
- index_embed_batch_chunks
+ - chunk_max_concurrent
+ - llama_cache_ram_mib
- source
properties:
embedding_model:
@@ -3270,6 +3272,14 @@ components:
type: integer
minimum: 0
description: Cross-file embed-batch size for repo indexing (chunks per embed call). 0 = one call per file.
+ chunk_max_concurrent:
+ type: integer
+ minimum: 0
+ description: Chunker (tree-sitter wasm) instance-concurrency cap, decoupled from embedding concurrency. Each instance holds ~69 MiB. 0 = recommended (3).
+ llama_cache_ram_mib:
+ type: integer
+ minimum: -1
+ description: llama-server host prompt-cache cap in MiB (--cache-ram). 0 = disabled (recommended for embeddings — prompts are never reused, and llama's upstream 8 GiB default grows RSS until the container OOMs), -1 = unlimited.
source:
type: object
additionalProperties:
@@ -3301,6 +3311,8 @@ components:
- max_embedding_concurrency
- llama_batch_size
- index_embed_batch_chunks
+ - chunk_max_concurrent
+ - llama_cache_ram_mib
properties:
embedding_model: { type: string }
llama_ctx_size: { type: integer }
@@ -3309,6 +3321,8 @@ components:
max_embedding_concurrency: { type: integer }
llama_batch_size: { type: integer }
index_embed_batch_chunks: { type: integer }
+ chunk_max_concurrent: { type: integer }
+ llama_cache_ram_mib: { type: integer }
RuntimeConfigUpdate:
type: object
@@ -3339,6 +3353,13 @@ components:
index_embed_batch_chunks:
type: integer
nullable: true
+ chunk_max_concurrent:
+ type: integer
+ nullable: true
+ llama_cache_ram_mib:
+ type: integer
+ nullable: true
+ description: MiB; 0 clears the override (falls back to env / recommended = disabled), -1 = unlimited.
SidecarStatus:
type: object
diff --git a/poc/wasm-treesitter/.gitignore b/poc/wasm-treesitter/.gitignore
new file mode 100644
index 0000000..551d24f
--- /dev/null
+++ b/poc/wasm-treesitter/.gitignore
@@ -0,0 +1,3 @@
+# build artifact — rebuilt by build.sh; the committed module lives as
+# server/internal/chunker/tswasm/ts-core.wasm.br (brotli)
+ts-core.wasm
diff --git a/poc/wasm-treesitter/README.md b/poc/wasm-treesitter/README.md
new file mode 100644
index 0000000..8d147c0
--- /dev/null
+++ b/poc/wasm-treesitter/README.md
@@ -0,0 +1,70 @@
+# PoC: tree-sitter via WASM/wazero (pure-Go, no cgo)
+
+An alternative to the cgo backend on `feat/chunker-cgo-treesitter`. The **official**
+tree-sitter C runtime + the official TypeScript grammar are compiled to a single
+standalone `wasm32-wasi` reactor module (`build.sh`, via `zig cc`) and driven
+from Go through [wazero](https://github.com/tetratelabs/wazero) — **no cgo, no
+JavaScript, no third-party parser**. Only `wasmts.go` (our wazero host) is
+bespoke; the parser itself is the unmodified upstream C.
+
+Goal: give us real **speed + stability** numbers to choose between cgo and wasm.
+
+## Results — same 852-file vscode TypeScript corpus, full-tree walk
+
+| backend | wall | files/s | MB/s | ERROR trees | `editorOptions.ts` |
+|---|---|---|---|---|---|
+| gotreesitter (pure-Go GLR) | 13.83 s | 62 | 0.8 | **13** | 8.77 s → ERROR |
+| **WASM (wazero, pure-Go host)** | **~2.5 s** | **~330** | **~4.1** | **0** | **49 ms** |
+| cgo (native tree-sitter) | 1.26 s | 675 | 11.5 | 0 | 17 ms |
+
+- **WASM is ~2× slower than cgo, ~5× faster than gotreesitter, and correct** (0 ERROR trees vs gotreesitter's 13).
+- The WASM overhead is the **host↔guest call boundary**, not memory: each of the
+ 2.68 M nodes costs ~3 wazero calls (`ts_node_type`, `ts_node_child_count`,
+ `ts_node_child`). Reusing node slots instead of `malloc`/`free` per node moved
+ the number only 328→357 files/s — so it's the calls. A single batched
+ "serialize subtree" export would close most of the remaining gap vs cgo
+ (future work; not done here).
+
+## Stability (`cmd/stability`)
+
+- tree-sitter is **robust**: 6 adversarial inputs (100–200 k-deep nesting, 5 MB
+ single token, invalid UTF-8, unbalanced templates) all parsed without crashing
+ — this is true of cgo too, so it is **not** a bug WASM uniquely fixes.
+- What WASM **adds** is containment: a guest-side fault (resource limit, and in
+ principle any C bug — stack overflow, OOB) surfaces as a **recoverable Go
+ error**; the host process stays alive. The memory-capped run demonstrates this.
+- Under cgo the equivalent fault is a native **SIGSEGV/abort that kills the whole
+ cix-server**. So crash-isolation is **insurance against unknown C bugs in
+ grammars/scanners**, not a fix for an observed crash.
+
+## Trade-off summary
+
+| | cgo (current) | WASM/wazero (this PoC) |
+|---|---|---|
+| Parse speed | 🟢 fastest | 🟡 ~2× slower (≈invisible end-to-end: embeddings dominate) |
+| Correctness | 🟢 official | 🟢 official (identical parser) |
+| Build | 🟡 needs C toolchain (musl-static solved it) | 🟢 `CGO_ENABLED=0`, trivial cross-compile; `zig` only at wasm-build time (one-off, artifact committed) |
+| Crash isolation | 🔴 C fault kills process | 🟢 contained → Go error |
+| Binary size | 🔴 ~78 MB (grammar tables linked natively) | 🟢 likely smaller: pure-Go host (~41 MB) + embedded `.wasm` (1.4 MB / grammar, brotli-compressible) |
+| Maturity / effort | 🟢 drop-in (official binding + 31 grammar modules) | 🔴 bespoke host; must build/bundle 31 grammar `.wasm` + flesh out node API + batched walk |
+
+## Honest read
+
+It's close. cgo is done and fastest. WASM costs ~2× on **parsing**, but since
+**embeddings dominate end-to-end indexing time**, that 2× is largely invisible in
+production — while WASM's upsides (no cgo, crash-isolation, smaller binary,
+toolchain-free server builds) are real. The price of WASM is **engineering
+effort** to productionize: build all 31 grammars into the module, write the full
+node-walk API the chunker needs (with a batched-walk export to recover speed),
+and wire it behind the same `tsgrammars`-style registry.
+
+## Build & run
+
+```bash
+brew install zig # provides clang + wasi-libc cross-compile
+./build.sh # → ts-ts.wasm (official tree-sitter v0.25.10 + tree-sitter-typescript v0.23.2)
+go run ./cmd/bench /path/to/vscode/src/vs/editor
+go run ./cmd/stability
+```
+
+`ts-ts.wasm` is committed so the benchmarks run without zig.
diff --git a/poc/wasm-treesitter/build.sh b/poc/wasm-treesitter/build.sh
new file mode 100755
index 0000000..5686b19
--- /dev/null
+++ b/poc/wasm-treesitter/build.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+# Builds ts-core.wasm: the OFFICIAL tree-sitter C runtime + the base grammars +
+# our host_extra.c (the batched ts_dump_tree walk), compiled to ONE standalone
+# wasm32-wasi reactor module via `zig cc`. No emscripten, no JS glue.
+#
+# Requires: zig (clang + wasi-libc cross-compile), git, and tree-sitter CLI (only
+# for grammars whose repo ships no committed parser.c — gen=1 rows).
+#
+# For wasm we compile each grammar IN PLACE from a full clone, so relative
+# includes (e.g. typescript's ../../common/scanner.h) and src-root headers (html
+# tag.h, haskell unicode.h) resolve naturally — none of the vendor.sh copy/rewrite
+# dance is needed. Quirks that remain: SHA pins (dart), `tree-sitter generate`
+# (sql), and a 2nd grammar from one repo (tsx). See plan §6.1.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+TS_VERSION="${TS_VERSION:-v0.25.10}"
+OUT="${OUT:-ts-core.wasm}"
+WORK="$(mktemp -d)"
+trap 'rm -rf "$WORK"' EXIT
+
+# id repo ref srcsubdir [gen]
+GRAMMARS=(
+ "python tree-sitter/tree-sitter-python v0.25.0 src"
+ "typescript tree-sitter/tree-sitter-typescript v0.23.2 typescript/src"
+ "tsx tree-sitter/tree-sitter-typescript v0.23.2 tsx/src"
+ "javascript tree-sitter/tree-sitter-javascript v0.25.0 src"
+ "go tree-sitter/tree-sitter-go v0.25.0 src"
+ "rust tree-sitter/tree-sitter-rust v0.24.2 src"
+ "java tree-sitter/tree-sitter-java v0.23.5 src"
+ "c tree-sitter/tree-sitter-c v0.24.2 src"
+ "cpp tree-sitter/tree-sitter-cpp v0.23.4 src"
+ "ruby tree-sitter/tree-sitter-ruby v0.23.1 src"
+ "c_sharp tree-sitter/tree-sitter-c-sharp v0.23.5 src"
+ "php tree-sitter/tree-sitter-php v0.24.2 php/src"
+ "swift alex-pinkus/tree-sitter-swift 0.7.3-with-generated-files src"
+ "kotlin tree-sitter-grammars/tree-sitter-kotlin v1.1.0 src"
+ "scala tree-sitter/tree-sitter-scala v0.26.0 src"
+ "bash tree-sitter/tree-sitter-bash v0.25.1 src"
+ "lua tree-sitter-grammars/tree-sitter-lua v0.5.0 src"
+ "dart UserNobody14/tree-sitter-dart a9bdfa3 src"
+ "r r-lib/tree-sitter-r v1.2.0 src"
+ "objc tree-sitter-grammars/tree-sitter-objc v3.0.2 src"
+ "html tree-sitter/tree-sitter-html v0.23.2 src"
+ "css tree-sitter/tree-sitter-css v0.25.0 src"
+ "scss tree-sitter-grammars/tree-sitter-scss v1.0.0 src"
+ "sql DerekStride/tree-sitter-sql v0.3.11 src 1"
+ "markdown tree-sitter-grammars/tree-sitter-markdown v0.5.3 tree-sitter-markdown/src"
+ "zig tree-sitter-grammars/tree-sitter-zig v1.1.2 src"
+ "julia tree-sitter/tree-sitter-julia v0.25.0 src"
+ "fortran stadelmanma/tree-sitter-fortran v0.6.0 src"
+ "haskell tree-sitter/tree-sitter-haskell v0.23.1 src"
+ "ocaml tree-sitter/tree-sitter-ocaml v0.25.0 grammars/ocaml/src"
+ "solidity JoranHonig/tree-sitter-solidity v1.2.13 src"
+)
+
+clone() { # repo ref dest — tag/branch fast path, SHA fallback
+ local repo="$1" ref="$2" dest="$3"
+ git clone --depth 1 --branch "$ref" "https://github.com/$repo" "$dest" >/dev/null 2>&1 && return 0
+ git clone "https://github.com/$repo" "$dest" >/dev/null 2>&1 || return 1
+ git -C "$dest" checkout "$ref" >/dev/null 2>&1
+}
+
+echo "→ tree-sitter runtime $TS_VERSION"
+git clone --depth 1 --branch "$TS_VERSION" https://github.com/tree-sitter/tree-sitter "$WORK/tree-sitter" 2>/dev/null
+
+SRCS=( "$WORK/tree-sitter/lib/src/lib.c" "csrc/host_extra.c" )
+INCS=( -I "$WORK/tree-sitter/lib/include" -I "$WORK/tree-sitter/lib/src" )
+EXPORTS=()
+BUILT=() ; FAILED=()
+
+for row in "${GRAMMARS[@]}"; do
+ read -r id repo ref sub gen <<<"$row"
+ printf ' %-12s %s@%s ' "$id" "$repo" "$ref"
+ if ! clone "$repo" "$ref" "$WORK/$id"; then echo "CLONE FAIL"; FAILED+=("$id"); continue; fi
+ gsrc="$WORK/$id/$sub"
+ if [ "${gen:-0}" = "1" ] && [ ! -f "$gsrc/parser.c" ]; then
+ ( cd "$WORK/$id" && tree-sitter generate >/dev/null 2>&1 ) || true
+ fi
+ if [ ! -f "$gsrc/parser.c" ]; then echo "NO parser.c"; FAILED+=("$id"); continue; fi
+ SRCS+=( "$gsrc/parser.c" )
+ [ -f "$gsrc/scanner.c" ] && SRCS+=( "$gsrc/scanner.c" )
+ [ -f "$gsrc/scanner.cc" ] && SRCS+=( "$gsrc/scanner.cc" )
+ INCS+=( -I "$gsrc" )
+ EXPORTS+=( -Wl,--export=tree_sitter_$id )
+ BUILT+=("$id")
+ echo "ok"
+done
+
+echo "→ compiling ${#SRCS[@]} sources, ${#BUILT[@]} grammars → $OUT"
+zig cc --target=wasm32-wasi-musl -mexec-model=reactor \
+ "${INCS[@]}" "${SRCS[@]}" \
+ -o "$OUT" -Oz -fPIC -Wl,--no-entry -Wl,--strip-debug \
+ -Wl,--export=malloc -Wl,--export=free \
+ -Wl,--export=ts_parser_new -Wl,--export=ts_parser_delete \
+ -Wl,--export=ts_parser_set_language -Wl,--export=ts_parser_parse_string \
+ -Wl,--export=ts_parser_reset \
+ -Wl,--export=ts_tree_delete -Wl,--export=ts_tree_root_node \
+ -Wl,--export=ts_node_child_count -Wl,--export=ts_node_child \
+ -Wl,--export=ts_node_type -Wl,--export=ts_node_start_byte \
+ -Wl,--export=ts_node_end_byte -Wl,--export=ts_node_has_error \
+ -Wl,--export=ts_dump_tree -Wl,--export=ts_dump_rec_size \
+ -Wl,--export=ts_language_symbol_count -Wl,--export=ts_language_symbol_name \
+ "${EXPORTS[@]}"
+
+echo "built $OUT ($(du -h "$OUT" | cut -f1)) — runtime $TS_VERSION, ${#BUILT[@]} grammars"
+[ ${#FAILED[@]} -gt 0 ] && echo "FAILED: ${FAILED[*]}"
+echo "grammars: ${BUILT[*]}"
diff --git a/poc/wasm-treesitter/cmd/bench/main.go b/poc/wasm-treesitter/cmd/bench/main.go
new file mode 100644
index 0000000..d416940
--- /dev/null
+++ b/poc/wasm-treesitter/cmd/bench/main.go
@@ -0,0 +1,82 @@
+// Command bench parses every .ts file under a directory with the WASM backend
+// and reports throughput — apples-to-apples with the cgo/gotreesitter numbers
+// in ../../README.md (same corpus, same full-tree walk).
+//
+// go run ./cmd/bench /path/to/vscode/src/vs/editor
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "time"
+
+ wasmts "github.com/dvcdsys/code-index/poc/wasm-treesitter"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Println("usage: bench ")
+ os.Exit(2)
+ }
+ root := os.Args[1]
+ ctx := context.Background()
+ eng, err := wasmts.New(ctx, 0)
+ if err != nil {
+ panic(err)
+ }
+ defer eng.Close()
+
+ var files []string
+ filepath.WalkDir(root, func(p string, d os.DirEntry, e error) error {
+ if e == nil && !d.IsDir() && filepath.Ext(p) == ".ts" {
+ files = append(files, p)
+ }
+ return nil
+ })
+ fmt.Printf("corpus: %d .ts files\n\n", len(files))
+
+ type res struct {
+ path string
+ dur time.Duration
+ isErr bool
+ }
+ var all []res
+ var totalParse time.Duration
+ var totalBytes, errFiles, totalNodes int
+ start := time.Now()
+ for _, f := range files {
+ src, _ := os.ReadFile(f)
+ t0 := time.Now()
+ r, err := eng.Parse("tree_sitter_typescript", src)
+ d := time.Since(t0)
+ if err != nil {
+ fmt.Printf(" trap on %s: %v\n", filepath.Base(f), err)
+ continue
+ }
+ totalParse += d
+ totalBytes += len(src)
+ totalNodes += r.Nodes
+ if r.HasError {
+ errFiles++
+ }
+ all = append(all, res{f, d, r.HasError})
+ }
+ wall := time.Since(start)
+ sort.Slice(all, func(i, j int) bool { return all[i].dur > all[j].dur })
+ mb := float64(totalBytes) / 1e6
+ fmt.Println("=== WASM: official tree-sitter via wazero (pure-Go host, no cgo) ===")
+ fmt.Printf(" wall: %v parse+walk: %v\n", wall.Round(time.Millisecond), totalParse.Round(time.Millisecond))
+ fmt.Printf(" throughput: %.0f files/s, %.1f MB/s\n", float64(len(files))/wall.Seconds(), mb/totalParse.Seconds())
+ fmt.Printf(" ERROR trees: %d / %d nodes walked: %d\n", errFiles, len(files), totalNodes)
+ fmt.Printf(" slowest 5:\n")
+ for i := 0; i < 5 && i < len(all); i++ {
+ e := ""
+ if all[i].isErr {
+ e = " [ERROR]"
+ }
+ fmt.Printf(" %8v %s%s\n", all[i].dur.Round(time.Millisecond), filepath.Base(all[i].path), e)
+ }
+}
diff --git a/poc/wasm-treesitter/cmd/stability/main.go b/poc/wasm-treesitter/cmd/stability/main.go
new file mode 100644
index 0000000..011f21a
--- /dev/null
+++ b/poc/wasm-treesitter/cmd/stability/main.go
@@ -0,0 +1,77 @@
+// Command stability feeds adversarial inputs to the WASM backend and shows the
+// host survives every one, and that a hard resource-limit fault surfaces as a
+// recoverable Go error rather than a process crash — the crash-isolation
+// property that cgo cannot offer (a C segfault/abort kills the whole process).
+//
+// go run ./cmd/stability
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ wasmts "github.com/dvcdsys/code-index/poc/wasm-treesitter"
+)
+
+func main() {
+ ctx := context.Background()
+ eng, err := wasmts.New(ctx, 0)
+ if err != nil {
+ panic(err)
+ }
+ defer eng.Close()
+
+ random := make([]byte, 200000)
+ for i := range random {
+ random[i] = byte(i*37 + 11)
+ }
+ adversarial := []struct {
+ name string
+ src []byte
+ }{
+ {"deeply nested [ ] x100000", []byte(strings.Repeat("[", 100000) + strings.Repeat("]", 100000))},
+ {"deeply nested ( x200000", []byte(strings.Repeat("(", 200000))},
+ {"deep ternary x50000", []byte("let x=" + strings.Repeat("a?b:", 50000) + "c")},
+ {"huge single token 5MB", []byte(strings.Repeat("z", 5_000_000))},
+ {"invalid UTF-8 / random 200KB", random},
+ {"unbalanced template `${ x80000", []byte(strings.Repeat("`${", 80000))},
+ }
+
+ fmt.Println("=== STABILITY: adversarial inputs — host must survive every one ===")
+ survived := 0
+ for _, a := range adversarial {
+ r, err := eng.Parse("tree_sitter_typescript", a.src)
+ status := fmt.Sprintf("parsed-ok (nodes=%d, hasError=%v)", r.Nodes, r.HasError)
+ if err != nil {
+ status = "TRAP CONTAINED → " + err.Error()
+ }
+ fmt.Printf(" %-30s (%7d B): host ALIVE, %s\n", a.name, len(a.src), status)
+ survived++
+ }
+ fmt.Printf("\n → host survived ALL %d adversarial inputs.\n", survived)
+
+ if r, err := eng.Parse("tree_sitter_typescript", []byte("function f(x: number) { return x + 1; }")); err == nil {
+ fmt.Printf(" → normal file after barrage: parsed-ok (nodes=%d) — host fully functional\n", r.Nodes)
+ }
+
+ // Hard resource containment: a memory-capped instance turns an over-limit
+ // parse into a Go error, not a host crash.
+ capped, err := wasmts.New(ctx, 24) // ~1.5 MB cap, below the module's static needs
+ if err != nil {
+ fmt.Printf(" → memory-capped (1.5MB) instance: over-limit surfaced as a Go error (contained): %s\n", firstline(err.Error()))
+ } else {
+ _, perr := capped.Parse("tree_sitter_typescript", []byte(strings.Repeat("const x=[1,2,3];\n", 50000)))
+ fmt.Printf(" → memory-capped big parse: contained err=%v — host ALIVE\n", perr != nil)
+ capped.Close()
+ }
+
+ fmt.Println("\n Contrast: under cgo, an equivalent guest fault (C stack overflow / OOM abort)\n is a native SIGSEGV/abort that kills the whole cix-server process.")
+}
+
+func firstline(s string) string {
+ if i := strings.IndexByte(s, '\n'); i >= 0 {
+ return s[:i]
+ }
+ return s
+}
diff --git a/poc/wasm-treesitter/csrc/host_extra.c b/poc/wasm-treesitter/csrc/host_extra.c
new file mode 100644
index 0000000..42cec34
--- /dev/null
+++ b/poc/wasm-treesitter/csrc/host_extra.c
@@ -0,0 +1,75 @@
+// host_extra.c — custom exports compiled INTO the wasm module alongside the
+// official tree-sitter runtime. The whole point is `ts_dump_tree`: it walks the
+// parsed tree ENTIRELY inside the guest and writes a flat pre-order array of
+// fixed-size records into linear memory in ONE shot. The host then does a single
+// Memory.Read and runs the chunker's node matching in pure Go — turning the
+// ~3 wazero calls-per-node of the naive walk into ~1 call per parse.
+//
+// See docs/wasm-treesitter-implementation-plan.md §7.2 / §7.3.
+#include "tree_sitter/api.h"
+#include
+
+// One record per node. ALL fields uint32 so the Go side reads 9 little-endian
+// uint32s with zero packing ambiguity (recSize = 36). kind_id is the TSSymbol
+// (fits in 16 bits; widened for clean alignment). The host resolves kind_id ->
+// kind-name once per language via ts_language_symbol_name (see below), so the
+// per-node string lookup never crosses the boundary.
+typedef struct {
+ uint32_t kind_id; // ts_node_symbol
+ uint32_t start_byte;
+ uint32_t end_byte;
+ uint32_t start_row; // ts_node_start_point().row (0-based line)
+ uint32_t start_col; // ts_node_start_point().column
+ uint32_t end_row; // ts_node_end_point().row
+ uint32_t end_col; // ts_node_end_point().column
+ uint32_t depth; // pre-order depth (parent reconstruction via depth stack)
+ uint32_t flags; // bit0 named, bit1 error, bit2 missing, bit3 extra
+} NodeRec;
+
+static void emit(NodeRec *out, uint32_t i, TSNode n, uint32_t depth) {
+ out[i].kind_id = ts_node_symbol(n);
+ out[i].start_byte = ts_node_start_byte(n);
+ out[i].end_byte = ts_node_end_byte(n);
+ TSPoint sp = ts_node_start_point(n);
+ out[i].start_row = sp.row;
+ out[i].start_col = sp.column;
+ TSPoint ep = ts_node_end_point(n);
+ out[i].end_row = ep.row;
+ out[i].end_col = ep.column;
+ out[i].depth = depth;
+ uint32_t f = 0;
+ if (ts_node_is_named(n)) f |= 1u;
+ if (ts_node_is_error(n)) f |= 2u;
+ if (ts_node_is_missing(n)) f |= 4u;
+ if (ts_node_is_extra(n)) f |= 8u;
+ out[i].flags = f;
+}
+
+// ts_dump_tree writes up to `cap` records and returns the TRUE node count. If the
+// return value > cap the buffer was too small: the host re-mallocs to the
+// returned count and calls again. Iterative pre-order DFS via a tree cursor — no
+// recursion, no per-node malloc, no boundary crossings.
+uint32_t ts_dump_tree(const TSTree *tree, NodeRec *out, uint32_t cap) {
+ if (tree == 0) return 0;
+ TSTreeCursor cur = ts_tree_cursor_new(ts_tree_root_node(tree));
+ uint32_t count = 0;
+ uint32_t depth = 0;
+ for (;;) {
+ if (count < cap) {
+ emit(out, count, ts_tree_cursor_current_node(&cur), depth);
+ }
+ count++;
+ if (ts_tree_cursor_goto_first_child(&cur)) { depth++; continue; }
+ for (;;) {
+ if (ts_tree_cursor_goto_next_sibling(&cur)) break;
+ if (!ts_tree_cursor_goto_parent(&cur)) {
+ ts_tree_cursor_delete(&cur);
+ return count;
+ }
+ depth--;
+ }
+ }
+}
+
+// recSize lets the host assert its struct layout matches the guest's.
+uint32_t ts_dump_rec_size(void) { return (uint32_t)sizeof(NodeRec); }
diff --git a/poc/wasm-treesitter/go.mod b/poc/wasm-treesitter/go.mod
new file mode 100644
index 0000000..98548cb
--- /dev/null
+++ b/poc/wasm-treesitter/go.mod
@@ -0,0 +1,7 @@
+module github.com/dvcdsys/code-index/poc/wasm-treesitter
+
+go 1.25.3
+
+require github.com/tetratelabs/wazero v1.12.0
+
+require golang.org/x/sys v0.44.0 // indirect
diff --git a/poc/wasm-treesitter/go.sum b/poc/wasm-treesitter/go.sum
new file mode 100644
index 0000000..a10a279
--- /dev/null
+++ b/poc/wasm-treesitter/go.sum
@@ -0,0 +1,4 @@
+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
diff --git a/poc/wasm-treesitter/grammars_test.go b/poc/wasm-treesitter/grammars_test.go
new file mode 100644
index 0000000..9ef7064
--- /dev/null
+++ b/poc/wasm-treesitter/grammars_test.go
@@ -0,0 +1,75 @@
+package wasmts
+
+import (
+ "context"
+ "testing"
+)
+
+// minimal valid-ish snippet per base grammar — enough to exercise load + parse +
+// per-language symbol-table resolution through the batched path. We assert the
+// language loads, parsing does not trap, and produces nodes; per-language SYMBOL
+// correctness (which kinds = function/class/...) is the next phase.
+var smoke = []struct {
+ id string
+ src string
+}{
+ {"python", "def f():\n pass\n"},
+ {"typescript", "function f(){}\n"},
+ {"tsx", "const x = 1;\n"},
+ {"javascript", "function f(){}\n"},
+ {"go", "package m\nfunc F(){}\n"},
+ {"rust", "fn f(){}\n"},
+ {"java", "class C{}\n"},
+ {"c", "int f(){return 0;}\n"},
+ {"cpp", "int f(){return 0;}\n"},
+ {"ruby", "def f\nend\n"},
+ {"c_sharp", "class C{}\n"},
+ {"php", "\n"},
+ {"css", "a{color:red}\n"},
+ {"scss", "a{color:red}\n"},
+ {"sql", "SELECT 1;\n"},
+ {"markdown", "# Title\n"},
+ {"zig", "fn f() void {}\n"},
+ {"julia", "function f() end\n"},
+ {"fortran", "program p\nend program p\n"},
+ {"haskell", "main = return ()\n"},
+ {"ocaml", "let f () = ()\n"},
+ {"solidity", "contract C {}\n"},
+}
+
+func TestAllGrammarsLoad(t *testing.T) {
+ eng, err := New(context.Background(), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer eng.Close()
+
+ if len(smoke) != 31 {
+ t.Fatalf("expected 31 grammars in smoke set, got %d", len(smoke))
+ }
+
+ for _, c := range smoke {
+ t.Run(c.id, func(t *testing.T) {
+ nodes, err := eng.ParseNodes("tree_sitter_"+c.id, []byte(c.src))
+ if err != nil {
+ t.Fatalf("parse trapped: %v", err)
+ }
+ if len(nodes) == 0 {
+ t.Fatal("no nodes produced")
+ }
+ // the root node must carry a resolved kind name (symbol table works)
+ if nodes[0].Kind == "" {
+ t.Errorf("root kind unresolved (symbol table empty?)")
+ }
+ })
+ }
+}
diff --git a/poc/wasm-treesitter/ts-ts.wasm b/poc/wasm-treesitter/ts-ts.wasm
new file mode 100755
index 0000000..9251fb8
Binary files /dev/null and b/poc/wasm-treesitter/ts-ts.wasm differ
diff --git a/poc/wasm-treesitter/wasmts.go b/poc/wasm-treesitter/wasmts.go
new file mode 100644
index 0000000..5a60ab0
--- /dev/null
+++ b/poc/wasm-treesitter/wasmts.go
@@ -0,0 +1,247 @@
+// Package wasmts is a pure-Go tree-sitter backend: the official tree-sitter C
+// runtime + N grammars + our host_extra.c (batched ts_dump_tree walk), compiled
+// to a standalone wasm32-wasi reactor module (see build.sh) and driven from Go
+// via wazero — no cgo, no JS.
+//
+// Production path: Parse the source, then ts_dump_tree walks the WHOLE tree
+// inside the guest and writes a flat pre-order []NodeRec into linear memory; the
+// host does ONE Memory.Read and decodes it. kind_id (TSSymbol) is resolved to a
+// kind name once per language via ts_language_symbol_name (cached). This replaces
+// the naive ~3-wazero-calls-per-node walk that made the PoC ~2x slower than cgo.
+package wasmts
+
+import (
+ "context"
+ "encoding/binary"
+ _ "embed"
+ "fmt"
+
+ "github.com/tetratelabs/wazero"
+ "github.com/tetratelabs/wazero/api"
+ "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
+)
+
+//go:embed ts-core.wasm
+var wasmBinary []byte
+
+// recSize must match sizeof(NodeRec) in host_extra.c (9 × uint32). Asserted
+// against ts_dump_rec_size() at New().
+const recSize = 36
+
+// Node is a decoded tree-sitter node from the batched dump.
+type Node struct {
+ KindID uint32
+ Kind string // resolved from KindID via the per-language symbol table
+ StartByte, EndByte uint32
+ StartRow, StartCol uint32
+ EndRow, EndCol uint32
+ Depth uint32
+ Named, Error, Missing, Extra bool
+}
+
+// Engine is a single wazero instance hosting the tree-sitter runtime + grammars.
+// NOT safe for concurrent use — one Engine per worker (see plan §8).
+type Engine struct {
+ ctx context.Context
+ rt wazero.Runtime
+ mod api.Module
+ mem api.Memory
+
+ malloc, free api.Function
+ parserNew, parserDelete, parserReset api.Function
+ setLang, parse, treeDelete api.Function
+ dumpTree api.Function
+ langSymCount, langSymName api.Function
+
+ langPtr map[string]uint32 // langExport -> TSLanguage*
+ symName map[string]map[uint32]string // langExport -> (symbol id -> kind name)
+}
+
+// New compiles and instantiates the wasm module. memLimitPages caps guest linear
+// memory (0 = wazero default); a runaway/oversized parse then traps and is
+// returned as a Go error instead of taking the process down.
+func New(ctx context.Context, memLimitPages uint32) (*Engine, error) {
+ cfg := wazero.NewRuntimeConfigCompiler()
+ if memLimitPages > 0 {
+ cfg = cfg.WithMemoryLimitPages(memLimitPages)
+ }
+ rt := wazero.NewRuntimeWithConfig(ctx, cfg)
+ wasi_snapshot_preview1.MustInstantiate(ctx, rt)
+ mod, err := rt.InstantiateWithConfig(ctx, wasmBinary,
+ wazero.NewModuleConfig().WithName("ts").WithStartFunctions("_initialize"))
+ if err != nil {
+ rt.Close(ctx)
+ return nil, fmt.Errorf("instantiate: %w", err)
+ }
+ e := &Engine{
+ ctx: ctx, rt: rt, mod: mod, mem: mod.Memory(),
+ malloc: mod.ExportedFunction("malloc"),
+ free: mod.ExportedFunction("free"),
+ parserNew: mod.ExportedFunction("ts_parser_new"),
+ parserDelete: mod.ExportedFunction("ts_parser_delete"),
+ parserReset: mod.ExportedFunction("ts_parser_reset"),
+ setLang: mod.ExportedFunction("ts_parser_set_language"),
+ parse: mod.ExportedFunction("ts_parser_parse_string"),
+ treeDelete: mod.ExportedFunction("ts_tree_delete"),
+ dumpTree: mod.ExportedFunction("ts_dump_tree"),
+ langSymCount: mod.ExportedFunction("ts_language_symbol_count"),
+ langSymName: mod.ExportedFunction("ts_language_symbol_name"),
+ langPtr: map[string]uint32{},
+ symName: map[string]map[uint32]string{},
+ }
+ if rs := e.call(mod.ExportedFunction("ts_dump_rec_size")); rs != recSize {
+ rt.Close(ctx)
+ return nil, fmt.Errorf("NodeRec size mismatch: guest=%d host=%d", rs, recSize)
+ }
+ return e, nil
+}
+
+func (e *Engine) Close() { e.rt.Close(e.ctx) }
+
+// call invokes a wasm export, surfacing a guest trap as a Go panic so the
+// caller's recover() can contain it.
+func (e *Engine) call(f api.Function, args ...uint64) uint64 {
+ r, err := f.Call(e.ctx, args...)
+ if err != nil {
+ panic(err)
+ }
+ if len(r) == 0 {
+ return 0
+ }
+ return r[0]
+}
+
+// language resolves (and caches) the TSLanguage* for a grammar export.
+func (e *Engine) language(export string) uint32 {
+ if p, ok := e.langPtr[export]; ok {
+ return p
+ }
+ p := uint32(e.call(e.mod.ExportedFunction(export)))
+ e.langPtr[export] = p
+ return p
+}
+
+// symbolNames builds (once per language) the symbol-id -> kind-name table so the
+// per-node kind lookup happens in pure Go, never across the wazero boundary.
+func (e *Engine) symbolNames(export string, lang uint32) map[uint32]string {
+ if m, ok := e.symName[export]; ok {
+ return m
+ }
+ count := uint32(e.call(e.langSymCount, uint64(lang)))
+ m := make(map[uint32]string, count)
+ for id := range count {
+ ptr := uint32(e.call(e.langSymName, uint64(lang), uint64(id)))
+ m[id] = e.readCStr(ptr)
+ }
+ e.symName[export] = m
+ return m
+}
+
+// ParseNodes parses src under the given grammar export and returns the whole tree
+// as a flat pre-order slice (batched via ts_dump_tree). A guest-side trap is
+// returned as an error; the Engine and host process stay alive.
+func (e *Engine) ParseNodes(langExport string, src []byte) (nodes []Node, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("wasm trap (contained): %v", r)
+ }
+ }()
+
+ lang := e.language(langExport)
+ parser := e.call(e.parserNew)
+ defer e.call(e.parserDelete, parser)
+ e.call(e.setLang, parser, uint64(lang))
+
+ sp := uint32(e.call(e.malloc, uint64(len(src)+1)))
+ e.mem.Write(sp, src)
+ e.mem.WriteByte(sp+uint32(len(src)), 0)
+ defer e.call(e.free, uint64(sp))
+
+ tree := e.call(e.parse, parser, 0, uint64(sp), uint64(len(src)))
+ if tree == 0 {
+ return nil, fmt.Errorf("parse returned null tree")
+ }
+ defer e.call(e.treeDelete, tree)
+
+ // Pass 1: count nodes (no writes). Pass 2: dump into an exact buffer.
+ n := uint32(e.call(e.dumpTree, tree, 0, 0))
+ if n == 0 {
+ return nil, nil
+ }
+ buf := uint32(e.call(e.malloc, uint64(n)*recSize))
+ defer e.call(e.free, uint64(buf))
+ got := uint32(e.call(e.dumpTree, tree, uint64(buf), uint64(n)))
+ if got != n {
+ return nil, fmt.Errorf("dump count changed between passes: %d vs %d", n, got)
+ }
+
+ raw, ok := e.mem.Read(buf, n*recSize)
+ if !ok {
+ return nil, fmt.Errorf("read dump buffer failed (ptr=%d len=%d)", buf, n*recSize)
+ }
+ names := e.symbolNames(langExport, lang)
+
+ nodes = make([]Node, n)
+ for i := range n {
+ o := i * recSize
+ kindID := binary.LittleEndian.Uint32(raw[o:])
+ flags := binary.LittleEndian.Uint32(raw[o+32:])
+ nodes[i] = Node{
+ KindID: kindID,
+ Kind: names[kindID],
+ StartByte: binary.LittleEndian.Uint32(raw[o+4:]),
+ EndByte: binary.LittleEndian.Uint32(raw[o+8:]),
+ StartRow: binary.LittleEndian.Uint32(raw[o+12:]),
+ StartCol: binary.LittleEndian.Uint32(raw[o+16:]),
+ EndRow: binary.LittleEndian.Uint32(raw[o+20:]),
+ EndCol: binary.LittleEndian.Uint32(raw[o+24:]),
+ Depth: binary.LittleEndian.Uint32(raw[o+28:]),
+ Named: flags&1 != 0,
+ Error: flags&2 != 0,
+ Missing: flags&4 != 0,
+ Extra: flags&8 != 0,
+ }
+ }
+ return nodes, nil
+}
+
+func (e *Engine) readCStr(ptr uint32) string {
+ if ptr == 0 {
+ return ""
+ }
+ var b []byte
+ for off := ptr; ; off++ {
+ c, ok := e.mem.ReadByte(off)
+ if !ok || c == 0 {
+ break
+ }
+ b = append(b, c)
+ }
+ return string(b)
+}
+
+// ParseResult holds summary counts from a parse (back-compat for cmd/bench and
+// cmd/stability).
+type ParseResult struct {
+ HasError bool
+ Nodes int
+ Errors int
+}
+
+// Parse is a thin summary wrapper over ParseNodes.
+func (e *Engine) Parse(langExport string, src []byte) (ParseResult, error) {
+ nodes, err := e.ParseNodes(langExport, src)
+ if err != nil {
+ return ParseResult{}, err
+ }
+ res := ParseResult{Nodes: len(nodes)}
+ for _, n := range nodes {
+ if n.Error {
+ res.Errors++
+ }
+ if n.Error || n.Missing {
+ res.HasError = true
+ }
+ }
+ return res, nil
+}
diff --git a/poc/wasm-treesitter/wasmts_test.go b/poc/wasm-treesitter/wasmts_test.go
new file mode 100644
index 0000000..ec75f67
--- /dev/null
+++ b/poc/wasm-treesitter/wasmts_test.go
@@ -0,0 +1,85 @@
+package wasmts
+
+import (
+ "context"
+ "strings"
+ "testing"
+)
+
+// findDecls returns the source text of every node whose kind is in want.
+func findDecls(src []byte, nodes []Node, want map[string]bool) map[string][]string {
+ out := map[string][]string{}
+ for _, n := range nodes {
+ if want[n.Kind] {
+ txt := string(src[n.StartByte:n.EndByte])
+ if i := strings.IndexByte(txt, '\n'); i >= 0 {
+ txt = txt[:i]
+ }
+ out[n.Kind] = append(out[n.Kind], txt)
+ }
+ }
+ return out
+}
+
+func TestParseNodes_MultiLanguage(t *testing.T) {
+ eng, err := New(context.Background(), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer eng.Close()
+
+ cases := []struct {
+ export string
+ src string
+ want []string // kinds that MUST appear
+ }{
+ {
+ "tree_sitter_go",
+ "package main\n\nfunc Hello() int { return 1 }\n\ntype T struct{ X int }\n",
+ []string{"function_declaration", "type_declaration"},
+ },
+ {
+ "tree_sitter_python",
+ "def hello():\n return 1\n\nclass C:\n def m(self):\n pass\n",
+ []string{"function_definition", "class_definition"},
+ },
+ {
+ "tree_sitter_typescript",
+ "export function f(x: number): number { return x }\nclass C { m() {} }\ninterface I { a: number }\n",
+ []string{"function_declaration", "class_declaration", "interface_declaration"},
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.export, func(t *testing.T) {
+ nodes, err := eng.ParseNodes(c.export, []byte(c.src))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(nodes) == 0 {
+ t.Fatal("no nodes")
+ }
+ // no parse errors on valid input
+ errs := 0
+ for _, n := range nodes {
+ if n.Error || n.Missing {
+ errs++
+ }
+ }
+ if errs != 0 {
+ t.Errorf("expected 0 error/missing nodes, got %d", errs)
+ }
+ want := map[string]bool{}
+ for _, k := range c.want {
+ want[k] = true
+ }
+ got := findDecls([]byte(c.src), nodes, want)
+ for _, k := range c.want {
+ if len(got[k]) == 0 {
+ t.Errorf("kind %q not found", k)
+ }
+ }
+ t.Logf("%s: %d nodes, decls=%v", c.export, len(nodes), got)
+ })
+ }
+}
diff --git a/server/cmd/cix-server/main.go b/server/cmd/cix-server/main.go
index 7bad5e0..b30c924 100644
--- a/server/cmd/cix-server/main.go
+++ b/server/cmd/cix-server/main.go
@@ -13,12 +13,14 @@ import (
_ "net/http/pprof" // opt-in heap/CPU profiling, exposed only when CIX_PPROF_ADDR is set
"os"
"os/signal"
+ "strconv"
"strings"
"syscall"
"time"
"github.com/dvcdsys/code-index/server/internal/apikeys"
"github.com/dvcdsys/code-index/server/internal/chunker"
+ "github.com/dvcdsys/code-index/server/internal/chunker/tswasm"
"github.com/dvcdsys/code-index/server/internal/config"
"github.com/dvcdsys/code-index/server/internal/db"
"github.com/dvcdsys/code-index/server/internal/embeddings"
@@ -82,6 +84,17 @@ func main() {
// parseLogLevel maps CIX_LOG_LEVEL (debug|info|warn|error, case-insensitive)
// to a slog level. Unset or unrecognised values fall back to info.
+// envPositiveInt returns the positive integer value of an env var, or 0 if unset
+// or unparsable. Used for optional numeric tuning knobs.
+func envPositiveInt(key string) int {
+ if s := os.Getenv(key); s != "" {
+ if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && n > 0 {
+ return n
+ }
+ }
+ return 0
+}
+
func parseLogLevel(s string) slog.Level {
switch strings.ToLower(strings.TrimSpace(s)) {
case "debug":
@@ -118,6 +131,21 @@ func run() error {
chunker.Configure(cfg.Languages)
logger.Info("chunker languages configured", "active", chunker.SupportedLanguages())
+ // tree-sitter (wasm) memory knobs. Each parser instance loads all grammar
+ // tables into its own linear memory (~69 MiB baseline). ChunkMaxConcurrent —
+ // the chunker's instance-concurrency cap — is dashboard-overridable, so it's
+ // applied below from the resolved runtime-config snapshot (not here). These
+ // per-instance memory knobs stay ENV-only (rarely tuned).
+ if v := envPositiveInt("CIX_CHUNK_MEM_LIMIT_PAGES"); v > 0 {
+ tswasm.MemLimitPages = uint32(v)
+ }
+ if v := envPositiveInt("CIX_CHUNK_RECYCLE_GROWTH_MB"); v > 0 {
+ tswasm.RecycleGrowthBytes = uint64(v) << 20
+ }
+ if v := envPositiveInt("CIX_CHUNK_MAX_IDLE"); v > 0 {
+ tswasm.MaxIdleInstances = v
+ }
+
// The system DB is model-INDEPENDENT (one permanent file at
// cfg.SQLitePath holding accounts + catalog + parsed code). Older
// builds suffixed the model name onto the path; adopt any such legacy
@@ -158,6 +186,10 @@ func run() error {
return fmt.Errorf("load runtime_settings: %w", err)
}
snap.ApplyTo(cfg)
+ // Apply the chunker instance-concurrency cap from the resolved snapshot
+ // (DB > env > recommended). Live changes from the dashboard re-apply via
+ // PutRuntimeConfig → tswasm.SetMaxConcurrent.
+ tswasm.SetMaxConcurrent(snap.ChunkMaxConcurrent)
logger.Info("runtime config resolved",
"embedding_model", cfg.EmbeddingModel,
"llama_ctx", cfg.LlamaCtxSize,
@@ -165,6 +197,7 @@ func run() error {
"n_threads", cfg.LlamaNThreads,
"max_concurrency", cfg.MaxEmbeddingConcurrency,
"batch", cfg.LlamaBatchSize,
+ "chunk_max_concurrent", snap.ChunkMaxConcurrent,
"sources", snap.Source,
)
// The system DB is model-independent (opened above at cfg.SQLitePath).
diff --git a/server/dashboard/src/modules/server/ServerPage.tsx b/server/dashboard/src/modules/server/ServerPage.tsx
index f492f3b..158adc6 100644
--- a/server/dashboard/src/modules/server/ServerPage.tsx
+++ b/server/dashboard/src/modules/server/ServerPage.tsx
@@ -28,6 +28,8 @@ interface Draft {
max_embedding_concurrency: number;
llama_batch_size: number;
index_embed_batch_chunks: number;
+ chunk_max_concurrent: number;
+ llama_cache_ram_mib: number;
}
function configToDraft(c: RuntimeConfig): Draft {
@@ -39,6 +41,8 @@ function configToDraft(c: RuntimeConfig): Draft {
max_embedding_concurrency: c.max_embedding_concurrency,
llama_batch_size: c.llama_batch_size,
index_embed_batch_chunks: c.index_embed_batch_chunks,
+ chunk_max_concurrent: c.chunk_max_concurrent,
+ llama_cache_ram_mib: c.llama_cache_ram_mib,
};
}
@@ -58,6 +62,8 @@ function diffPatch(c: RuntimeConfig, d: Draft): { patch: RuntimeConfigUpdate; ch
'max_embedding_concurrency',
'llama_batch_size',
'index_embed_batch_chunks',
+ 'chunk_max_concurrent',
+ 'llama_cache_ram_mib',
] as const) {
if (d[k] !== c[k]) {
patch[k] = d[k];
@@ -203,9 +209,11 @@ export default function ServerPage() {
draftCtx={draft.llama_ctx_size}
draftGpuLayers={draft.llama_n_gpu_layers}
draftThreads={draft.llama_n_threads}
+ draftCacheRAM={draft.llama_cache_ram_mib}
onDraftCtx={(n) => setDraft({ ...draft, llama_ctx_size: n })}
onDraftGpuLayers={(n) => setDraft({ ...draft, llama_n_gpu_layers: n })}
onDraftThreads={(n) => setDraft({ ...draft, llama_n_threads: n })}
+ onDraftCacheRAM={(n) => setDraft({ ...draft, llama_cache_ram_mib: n })}
/>
@@ -224,9 +232,11 @@ export default function ServerPage() {
draftConcurrency={draft.max_embedding_concurrency}
draftBatch={draft.llama_batch_size}
draftIndexBatch={draft.index_embed_batch_chunks}
+ draftChunkConc={draft.chunk_max_concurrent}
onDraftConcurrency={(n) => setDraft({ ...draft, max_embedding_concurrency: n })}
onDraftBatch={(n) => setDraft({ ...draft, llama_batch_size: n })}
onDraftIndexBatch={(n) => setDraft({ ...draft, index_embed_batch_chunks: n })}
+ onDraftChunkConc={(n) => setDraft({ ...draft, chunk_max_concurrent: n })}
isOllama={showOllamaSections}
/>
diff --git a/server/dashboard/src/modules/server/sections/AdvancedSection.tsx b/server/dashboard/src/modules/server/sections/AdvancedSection.tsx
index a71794a..c952511 100644
--- a/server/dashboard/src/modules/server/sections/AdvancedSection.tsx
+++ b/server/dashboard/src/modules/server/sections/AdvancedSection.tsx
@@ -10,9 +10,11 @@ interface Props {
draftConcurrency: number;
draftBatch: number;
draftIndexBatch: number;
+ draftChunkConc: number;
onDraftConcurrency: (n: number) => void;
onDraftBatch: (n: number) => void;
onDraftIndexBatch: (n: number) => void;
+ onDraftChunkConc: (n: number) => void;
// isOllama controls whether the llama-only batch-size field is
// rendered. Concurrency (the Service-level queue depth) applies to
// every provider — caps how many parallel /v1/embeddings POSTs go
@@ -29,14 +31,17 @@ export function AdvancedSection({
draftConcurrency,
draftBatch,
draftIndexBatch,
+ draftChunkConc,
onDraftConcurrency,
onDraftBatch,
onDraftIndexBatch,
+ onDraftChunkConc,
isOllama,
}: Props) {
const concId = useId();
const batchId = useId();
const idxBatchId = useId();
+ const chunkConcId = useId();
const rec = config?.recommended;
const src = config?.source;
@@ -122,6 +127,37 @@ export function AdvancedSection({
+
diff --git a/server/dashboard/src/modules/server/sections/RuntimeParamsSection.tsx b/server/dashboard/src/modules/server/sections/RuntimeParamsSection.tsx
index ac8fb5e..d36475f 100644
--- a/server/dashboard/src/modules/server/sections/RuntimeParamsSection.tsx
+++ b/server/dashboard/src/modules/server/sections/RuntimeParamsSection.tsx
@@ -51,9 +51,11 @@ interface Props {
draftCtx: number;
draftGpuLayers: number;
draftThreads: number;
+ draftCacheRAM: number;
onDraftCtx: (n: number) => void;
onDraftGpuLayers: (n: number) => void;
onDraftThreads: (n: number) => void;
+ onDraftCacheRAM: (n: number) => void;
}
// RuntimeParamsSection: ctx, n_gpu_layers, n_threads form. n_gpu_layers
@@ -64,9 +66,11 @@ export function RuntimeParamsSection({
draftCtx,
draftGpuLayers,
draftThreads,
+ draftCacheRAM,
onDraftCtx,
onDraftGpuLayers,
onDraftThreads,
+ onDraftCacheRAM,
}: Props) {
const rec = config?.recommended;
const src = config?.source;
@@ -109,6 +113,16 @@ export function RuntimeParamsSection({
source={src?.llama_n_threads}
onChange={onDraftThreads}
/>
+
);
diff --git a/server/go.mod b/server/go.mod
index 665ec3a..3418a09 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -3,13 +3,14 @@ module github.com/dvcdsys/code-index/server
go 1.25.11
require (
+ github.com/andybalholm/brotli v1.2.1
github.com/getkin/kin-openapi v0.135.0
github.com/go-chi/chi/v5 v5.2.4
github.com/go-git/go-git/v5 v5.19.0
github.com/google/uuid v1.6.0
github.com/oapi-codegen/runtime v1.4.0
- github.com/odvcencio/gotreesitter v0.20.2
github.com/philippgille/chromem-go v0.7.0
+ github.com/tetratelabs/wazero v1.12.0
golang.org/x/crypto v0.52.0
golang.org/x/sync v0.20.0
golang.org/x/time v0.15.0
diff --git a/server/go.sum b/server/go.sum
index 4464f61..1b989f5 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
+github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
@@ -120,8 +122,6 @@ github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
-github.com/odvcencio/gotreesitter v0.20.2 h1:oWxGgy0WzLJKeiZB8EFzXDDlHYG/hqiZmWrW+81uwy4=
-github.com/odvcencio/gotreesitter v0.20.2/go.mod h1:hBVkghd0paaYAVwd2087vfwdeU984bQbMo9LvpE0moo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -168,6 +168,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
@@ -176,6 +178,8 @@ github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQs
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
diff --git a/server/internal/chunker/chunker.go b/server/internal/chunker/chunker.go
index f8768b0..6391ba1 100644
--- a/server/internal/chunker/chunker.go
+++ b/server/internal/chunker/chunker.go
@@ -11,13 +11,11 @@ package chunker
import (
"log/slog"
+ "path/filepath"
"strings"
"sync"
- "sync/atomic"
- "time"
- sitter "github.com/odvcencio/gotreesitter"
- "github.com/odvcencio/gotreesitter/grammars"
+ "github.com/dvcdsys/code-index/server/internal/chunker/tswasm"
)
// maxChunkSize is the default maximum chunk size in bytes (chars).
@@ -35,22 +33,12 @@ const (
// minRefNameLength mirrors MIN_REF_NAME_LENGTH in chunker.py.
const minRefNameLength = 2
-// parseBudget caps wall-clock time spent in tree-sitter for a single file.
-// Some grammars (notably bash) have catastrophic-backtracking pathologies on
-// specific inputs — install.sh in this very repo took 31s to parse before
-// this guard. The parser's own SetTimeoutMicros checkpoint is best-effort
-// and overshoots by 3-4×, so we set the hint generously and rely on the
-// post-parse wall-clock check to decide whether to keep the tree.
-//
-// On overshoot we fall back to sliding-window chunks. We accept the wasted
-// CPU (parser keeps running until its next checkpoint) because killing a
-// pure-Go parse from outside is not safe — the only practical levers are
-// SetTimeoutMicros and the cancellation flag, both with the same overshoot
-// characteristic.
-const (
- parseBudget = 2 * time.Second
- parseHint = uint64(parseBudget / time.Microsecond)
-)
+// NOTE: the official tree-sitter (via tswasm) does not have gotreesitter's
+// catastrophic-backtracking pathology, so the old SetTimeoutMicros +
+// cancellation-flag wall-clock guard is gone. A watchdog-that-recycles-the-
+// instance on a hard deadline is the Phase-4 equivalent (plan §7.4); until then
+// a guest trap (e.g. memory cap) is caught by tswasm and falls back to the
+// sliding window.
// ---------------------------------------------------------------------------
// Language registry — built from defaultRegistry() at init() and reduced by
@@ -58,19 +46,17 @@ const (
// stay package-private; the engine reads them directly.
// ---------------------------------------------------------------------------
-// languageEntry bundles the three pieces of state a language needs.
+// languageEntry bundles the per-language chunker state. The grammar itself lives
+// in the tswasm module, addressed by the export name "tree_sitter_" derived
+// from the registry key — so no factory field is needed.
type languageEntry struct {
- factory languageFunc
- nodes map[string][]string // function|class|method|type → AST node types
- identifiers map[string]struct{} // identifier leaf-node types for ref extraction
+ nodes map[string][]string // function|class|method|type → AST node types
+ identifiers map[string]struct{} // identifier leaf-node types for ref extraction
}
-// languageFunc is a factory for sitter.Language.
-type languageFunc func() *sitter.Language
-
var (
registryMu sync.RWMutex
- languageRegistry map[string]languageFunc
+ languageRegistry map[string]string // lang id → tswasm export ("tree_sitter_")
languageNodes map[string]map[string][]string
identifierNodes map[string]map[string]struct{}
)
@@ -102,7 +88,7 @@ func Configure(enabled []string) {
}
}
- reg := make(map[string]languageFunc, len(defaults))
+ reg := make(map[string]string, len(defaults))
nodes := make(map[string]map[string][]string, len(defaults))
idents := make(map[string]map[string]struct{}, len(defaults))
@@ -112,7 +98,7 @@ func Configure(enabled []string) {
continue
}
}
- reg[lang] = entry.factory
+ reg[lang] = "tree_sitter_" + lang
if entry.nodes != nil {
nodes[lang] = entry.nodes
}
@@ -163,7 +149,6 @@ func defaultRegistry() map[string]languageEntry {
return map[string]languageEntry{
// --- Tier 1: original 6, kept as-is for parity with legacy Python ---
"python": {
- factory: grammars.PythonLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"class_definition"},
@@ -171,7 +156,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID(),
},
"typescript": {
- factory: grammars.TypescriptLanguage,
nodes: map[string][]string{
"function": {"function_declaration", "arrow_function"},
"class": {"class_declaration"},
@@ -181,7 +165,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "property_identifier"),
},
"javascript": {
- factory: grammars.JavascriptLanguage,
nodes: map[string][]string{
"function": {"function_declaration", "arrow_function"},
"class": {"class_declaration"},
@@ -190,7 +173,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("property_identifier"),
},
"go": {
- factory: grammars.GoLanguage,
nodes: map[string][]string{
"function": {"function_declaration"},
"method": {"method_declaration"},
@@ -199,7 +181,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "field_identifier"),
},
"rust": {
- factory: grammars.RustLanguage,
nodes: map[string][]string{
"function": {"function_item"},
"class": {"struct_item", "enum_item"},
@@ -208,7 +189,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "field_identifier"),
},
"java": {
- factory: grammars.JavaLanguage,
nodes: map[string][]string{
"function": {"method_declaration"},
"class": {"class_declaration"},
@@ -219,7 +199,6 @@ func defaultRegistry() map[string]languageEntry {
// --- Tier 2: bug-fix — grammars were registered, node maps were not ---
"tsx": {
- factory: grammars.TsxLanguage,
nodes: map[string][]string{
"function": {"function_declaration", "arrow_function"},
"class": {"class_declaration"},
@@ -229,7 +208,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "property_identifier"),
},
"c": {
- factory: grammars.CLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"struct_specifier"},
@@ -238,7 +216,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "field_identifier"),
},
"cpp": {
- factory: grammars.CppLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"class_specifier", "struct_specifier"},
@@ -247,7 +224,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "field_identifier"),
},
"ruby": {
- factory: grammars.RubyLanguage,
nodes: map[string][]string{
"function": {"method", "singleton_method"},
"class": {"class", "module"},
@@ -257,7 +233,6 @@ func defaultRegistry() map[string]languageEntry {
// --- Tier 3: mainstream additions, high confidence in node names ---
"c_sharp": {
- factory: grammars.CSharpLanguage,
nodes: map[string][]string{
"function": {"local_function_statement"},
"class": {"class_declaration"},
@@ -267,7 +242,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier"),
},
"php": {
- factory: grammars.PhpLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"class_declaration"},
@@ -277,7 +251,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("name", "variable_name"),
},
"swift": {
- factory: grammars.SwiftLanguage,
nodes: map[string][]string{
"function": {"function_declaration"},
"class": {"class_declaration"},
@@ -286,7 +259,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("simple_identifier", "type_identifier"),
},
"kotlin": {
- factory: grammars.KotlinLanguage,
nodes: map[string][]string{
"function": {"function_declaration"},
"class": {"class_declaration", "object_declaration"},
@@ -294,7 +266,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier", "simple_identifier"),
},
"scala": {
- factory: grammars.ScalaLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"class_definition", "object_definition"},
@@ -303,21 +274,18 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier"),
},
"bash": {
- factory: grammars.BashLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
},
identifiers: idID("variable_name", "word"),
},
"lua": {
- factory: grammars.LuaLanguage,
nodes: map[string][]string{
"function": {"function_declaration", "function_definition"},
},
identifiers: idID(),
},
"dart": {
- factory: grammars.DartLanguage,
nodes: map[string][]string{
"function": {"function_signature"},
"class": {"class_definition"},
@@ -327,14 +295,12 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier"),
},
"r": {
- factory: grammars.RLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
},
identifiers: idID(),
},
"objc": {
- factory: grammars.ObjcLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
"class": {"class_interface", "class_implementation"},
@@ -346,21 +312,18 @@ func defaultRegistry() map[string]languageEntry {
// --- Tier 4: markup / data / config with structural nodes ---
"html": {
- factory: grammars.HtmlLanguage,
nodes: map[string][]string{
"type": {"doctype"},
},
identifiers: nil,
},
"css": {
- factory: grammars.CssLanguage,
nodes: map[string][]string{
"class": {"rule_set"},
},
identifiers: nil,
},
"scss": {
- factory: grammars.ScssLanguage,
nodes: map[string][]string{
"function": {"mixin_statement"},
"class": {"rule_set"},
@@ -368,15 +331,15 @@ func defaultRegistry() map[string]languageEntry {
identifiers: nil,
},
"sql": {
- factory: grammars.SqlLanguage,
+ // DerekStride/tree-sitter-sql node names (verified against the
+ // grammar; differ from the gotreesitter bundle's *_statement names).
nodes: map[string][]string{
- "function": {"create_function_statement"},
- "type": {"create_table_statement"},
+ "function": {"create_function"},
+ "type": {"create_table", "create_view", "create_type", "create_index", "create_materialized_view"},
},
identifiers: nil,
},
"markdown": {
- factory: grammars.MarkdownLanguage,
nodes: map[string][]string{
// `section` already wraps the heading + body in
// tree-sitter-markdown — adding `atx_heading` would emit
@@ -388,7 +351,6 @@ func defaultRegistry() map[string]languageEntry {
// --- Tier 5: medium-confidence additions ---
"zig": {
- factory: grammars.ZigLanguage,
nodes: map[string][]string{
"function": {"function_declaration"},
"class": {"struct_declaration"},
@@ -396,14 +358,12 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID(),
},
"julia": {
- factory: grammars.JuliaLanguage,
nodes: map[string][]string{
"function": {"function_definition"},
},
identifiers: idID(),
},
"fortran": {
- factory: grammars.FortranLanguage,
nodes: map[string][]string{
"function": {"subroutine", "function"},
"class": {"module"},
@@ -411,7 +371,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID(),
},
"haskell": {
- factory: grammars.HaskellLanguage,
nodes: map[string][]string{
// `function` = untyped top-level def; `bind` = typed binding
// (signature + match together); `signature` is loose stand-alone
@@ -424,7 +383,6 @@ func defaultRegistry() map[string]languageEntry {
},
},
"ocaml": {
- factory: grammars.OcamlLanguage,
nodes: map[string][]string{
"function": {"value_definition"},
"class": {"module_definition"},
@@ -433,7 +391,6 @@ func defaultRegistry() map[string]languageEntry {
identifiers: idID("type_identifier"),
},
"solidity": {
- factory: grammars.SolidityLanguage,
nodes: map[string][]string{
"function": {"function_definition", "modifier_definition", "constructor_definition", "fallback_receive_definition"},
"class": {"contract_declaration", "library_declaration"},
@@ -532,10 +489,41 @@ func chunkFallback(filePath, content, language string) []Chunk {
// Tree-sitter path
// ---------------------------------------------------------------------------
+// minifiedMaxLineLen flags a file as minified when any single line exceeds it.
+// Hand-written code essentially never has 2 KB lines; minified/bundled JS and
+// CSS routinely pack the whole file into one. Applied only to the web-asset
+// languages where minification exists — long lines in other languages (e.g.
+// generated Go with embedded literals) still parse fine and stay on the AST path.
+const minifiedMaxLineLen = 2048
+
+// looksMinified reports whether a JS/TS/CSS-family file is minified or bundled
+// output: a ".min."-style name, or any line longer than minifiedMaxLineLen.
+func looksMinified(path, content, language string) bool {
+ switch language {
+ case "javascript", "typescript", "tsx", "css", "scss":
+ default:
+ return false
+ }
+ base := strings.ToLower(filepath.Base(path))
+ if strings.Contains(base, ".min.") || strings.HasSuffix(base, ".bundle.js") {
+ return true
+ }
+ lineStart := 0
+ for i := 0; i < len(content); i++ {
+ if content[i] == '\n' {
+ if i-lineStart > minifiedMaxLineLen {
+ return true
+ }
+ lineStart = i + 1
+ }
+ }
+ return len(content)-lineStart > minifiedMaxLineLen
+}
+
func chunkWithTreesitter(filePath, content, language string, maxSize int) ([]Chunk, []Reference, error) {
// Snapshot under RLock so a concurrent Configure() call does not race the read.
registryMu.RLock()
- langFn, ok := languageRegistry[language]
+ export, ok := languageRegistry[language]
nodeKinds := languageNodes[language]
idTypes := identifierNodes[language]
registryMu.RUnlock()
@@ -543,15 +531,18 @@ func chunkWithTreesitter(filePath, content, language string, maxSize int) ([]Chu
if !ok {
return chunkFallback(filePath, content, language), nil, nil
}
- lang := langFn()
- if lang == nil {
- return chunkFallback(filePath, content, language), nil, nil
- }
-
if nodeKinds == nil {
// Grammar exists but we don't have node definitions → sliding window.
return chunkFallback(filePath, content, language), nil, nil
}
+ if looksMinified(filePath, content, language) {
+ // Minified/bundled sources are the parser's pathological case: a
+ // 500 KB single-line bundle yields a huge tree, balloons the wasm
+ // instance to its memory cap, and forces a pool recycle — all to
+ // produce AST chunks with near-zero semantic-search value. Skip
+ // straight to the sliding window.
+ return chunkFallback(filePath, content, language), nil, nil
+ }
// Build flat target → kind map.
targetTypes := map[string]string{}
@@ -562,58 +553,27 @@ func chunkWithTreesitter(filePath, content, language string, maxSize int) ([]Chu
}
src := []byte(content)
- parser := sitter.NewParser(lang)
-
- // Twin guards: SetTimeoutMicros is the parser's own checkpoint-based
- // budget; the cancellation flag is set by an external timer when the
- // wall-clock deadline expires. The parser checks both at the same
- // granularity, so they overshoot together — we still rely on the
- // post-parse wall-clock check below to decide whether the tree is
- // trustworthy.
- parser.SetTimeoutMicros(parseHint)
- var cancelFlag uint32
- parser.SetCancellationFlag(&cancelFlag)
- deadline := time.AfterFunc(parseBudget, func() {
- atomic.StoreUint32(&cancelFlag, 1)
- })
-
- parseStart := time.Now()
- tree, err := parser.Parse(src)
- parseElapsed := time.Since(parseStart)
- deadline.Stop()
-
- // Hard wall-clock check — even if parser claims success, a tree that
- // took >2× the budget is the result of a backtracking pathology and
- // the structure is not trustworthy enough to chunk on. Falling back to
- // sliding window keeps the indexer responsive.
- if parseElapsed > 2*parseBudget {
- slog.Warn("chunker: parse exceeded budget, falling back to sliding window",
- "path", filePath, "language", language, "elapsed", parseElapsed,
- "budget", parseBudget)
+ nodes, err := tswasm.ParseNodes(export, src)
+ if err != nil {
+ // Unknown grammar export or a contained guest trap (e.g. memory cap) →
+ // fall back to sliding window so the file is still indexed.
+ slog.Warn("chunker: wasm parse failed, falling back to sliding window",
+ "path", filePath, "language", language, "err", err)
return chunkFallback(filePath, content, language), nil, nil
}
- if atomic.LoadUint32(&cancelFlag) == 1 {
- slog.Warn("chunker: parse cancelled by deadline, falling back to sliding window",
- "path", filePath, "language", language, "elapsed", parseElapsed)
+ if len(nodes) == 0 {
return chunkFallback(filePath, content, language), nil, nil
}
-
- if err != nil {
- return nil, nil, err
- }
- root := tree.RootNode()
- if root == nil {
- return nil, nil, nil
- }
+ tree := buildFlatTree(nodes)
lines := splitLines(content)
var chunks []Chunk
var coveredRanges [][2]int
- extractNodes(root, lang, src, targetTypes, lines, filePath, language, &chunks, &coveredRanges, nil)
+ extractNodes(tree, 0, src, targetTypes, lines, filePath, language, &chunks, &coveredRanges, nil)
// Extract references using the snapshotted identifier set.
- refs := extractReferences(root, lang, src, targetTypes, idTypes, filePath, language)
+ refs := extractReferences(tree, src, targetTypes, idTypes, filePath, language)
// Fill gaps between extracted symbol nodes with "module" chunks.
sortRanges(coveredRanges)
@@ -649,10 +609,43 @@ func chunkWithTreesitter(filePath, content, language string, maxSize int) ([]Chu
return finalChunks, refs, nil
}
+// flatTree is a lightweight tree over the flat pre-order []tswasm.Node from the
+// batched walk. children/parents are reconstructed once from each node's depth
+// so the extraction below can navigate like the old *sitter.Node tree.
+type flatTree struct {
+ nodes []tswasm.Node
+ children [][]int32
+ parents []int32
+}
+
+func buildFlatTree(nodes []tswasm.Node) *flatTree {
+ ft := &flatTree{
+ nodes: nodes,
+ children: make([][]int32, len(nodes)),
+ parents: make([]int32, len(nodes)),
+ }
+ stack := make([]int32, 0, 32) // current ancestor chain (pre-order DFS)
+ for i := range nodes {
+ d := nodes[i].Depth
+ for len(stack) > 0 && ft.nodes[stack[len(stack)-1]].Depth >= d {
+ stack = stack[:len(stack)-1]
+ }
+ if len(stack) > 0 {
+ p := stack[len(stack)-1]
+ ft.children[p] = append(ft.children[p], int32(i))
+ ft.parents[i] = p
+ } else {
+ ft.parents[i] = -1
+ }
+ stack = append(stack, int32(i))
+ }
+ return ft
+}
+
// extractNodes walks the AST and appends symbol chunks.
func extractNodes(
- node *sitter.Node,
- lang *sitter.Language,
+ ft *flatTree,
+ idx int,
src []byte,
targetTypes map[string]string,
lines []string,
@@ -661,14 +654,18 @@ func extractNodes(
coveredRanges *[][2]int,
parentName *string,
) {
- if node == nil {
- return
- }
- nodeType := node.Type(lang)
-
- if kind, ok := targetTypes[nodeType]; ok {
- startLine := int(node.StartPoint().Row)
- endLine := int(node.EndPoint().Row)
+ node := ft.nodes[idx]
+
+ if kind, ok := targetTypes[node.Kind]; ok {
+ // Pull the declaration's doc comment into its chunk. Without this,
+ // the comment lands in the gap BETWEEN symbol chunks and the gap
+ // filler emits it as a standalone micro "module" chunk — a generated
+ // file like openapi.gen.go produced 377 comment-only chunks of ~60 B
+ // each. Attached, the comment both disappears as noise and improves
+ // the chunk's embedding (the "what is this" prose sits with its code).
+ startLine := int(leadingCommentStart(ft, idx))
+ declLine := int(node.StartRow)
+ endLine := int(node.EndRow)
content := joinLines(lines[startLine : endLine+1])
@@ -678,10 +675,12 @@ func extractNodes(
actualKind = "method"
}
- symName := extractName(node, lang, src)
+ symName := extractName(ft, idx, src)
var sig *string
- if startLine < len(lines) {
- s := trimSpace(lines[startLine])
+ if declLine < len(lines) {
+ // Signature is the DECLARATION line, not the comment the chunk
+ // may now start with.
+ s := trimSpace(lines[declLine])
sig = &s
}
@@ -704,26 +703,77 @@ func extractNodes(
if currentParent == nil {
currentParent = parentName
}
- cnt := node.ChildCount()
- for i := 0; i < cnt; i++ {
- extractNodes(node.Child(i), lang, src, targetTypes, lines, filePath, language, chunks, coveredRanges, currentParent)
+ for _, c := range ft.children[idx] {
+ extractNodes(ft, int(c), src, targetTypes, lines, filePath, language, chunks, coveredRanges, currentParent)
}
return
}
}
- cnt := node.ChildCount()
- for i := 0; i < cnt; i++ {
- extractNodes(node.Child(i), lang, src, targetTypes, lines, filePath, language, chunks, coveredRanges, parentName)
+ for _, c := range ft.children[idx] {
+ extractNodes(ft, int(c), src, targetTypes, lines, filePath, language, chunks, coveredRanges, parentName)
}
}
-// extractReferences walks AST collecting identifier usages (not definitions).
-// idNodeTypes is passed in (rather than read from the global map) so callers
-// can snapshot once and stay consistent if Configure() is called concurrently.
+// leadingCommentStart returns the chunk start row for the node at idx,
+// extended upward over any directly-adjacent leading comment siblings — the
+// doc comment(s) of the declaration. Tree-sitter marks comments as "extra"
+// nodes (the Extra flag survives the flat dump), so this is language-agnostic:
+// Go doc comments, JSDoc blocks, Rust ///, Python # headers all qualify.
+//
+// Adjacency is strict: each comment must END on the line directly above
+// where the chunk currently starts. A blank line between the comment and the
+// declaration breaks the chain — that comment is free-standing prose and
+// stays in the surrounding module gap (e.g. a section banner).
+// The comment usually neighbours a WRAPPER of the target node rather than the
+// node itself — Go's type_spec sits inside type_declaration, and the doc
+// comment is the declaration's sibling. So when no comment is found among the
+// node's own siblings, climb through ancestors that start on the same row
+// (such single-row wrappers are pure syntax shells) and look again.
+func leadingCommentStart(ft *flatTree, idx int) uint32 {
+ declRow := ft.nodes[idx].StartRow
+ start := declRow
+ n := idx
+ for {
+ p := ft.parents[n]
+ if p < 0 {
+ return start
+ }
+ sibs := ft.children[p]
+ pos := -1
+ for i, c := range sibs {
+ if int(c) == n {
+ pos = i
+ break
+ }
+ }
+ for i := pos - 1; i >= 0; i-- {
+ s := ft.nodes[sibs[i]]
+ // Adjacent = the comment ends on the line directly above the
+ // current start, OR on the start line itself — some grammars
+ // (tree-sitter-rust line_comment) consume the trailing newline,
+ // so a `/// doc` above `fn foo` ends ON foo's row.
+ if !s.Extra || (s.EndRow+1 != start && s.EndRow != start) {
+ break
+ }
+ start = s.StartRow
+ }
+ if start != declRow {
+ return start // found the doc comment chain at this level
+ }
+ if ft.nodes[p].StartRow != declRow {
+ return start // parent is not a same-row wrapper — stop climbing
+ }
+ n = int(p)
+ }
+}
+
+// extractReferences walks the tree collecting identifier usages (not
+// definitions). idNodeTypes is passed in (rather than read from the global map)
+// so callers can snapshot once and stay consistent if Configure() is called
+// concurrently.
func extractReferences(
- root *sitter.Node,
- lang *sitter.Language,
+ ft *flatTree,
src []byte,
targetTypes map[string]string,
idNodeTypes map[string]struct{},
@@ -736,63 +786,49 @@ func extractReferences(
var refs []Reference
seen := map[[3]any]struct{}{}
- var walk func(n *sitter.Node)
- walk = func(n *sitter.Node) {
- if n == nil {
- return
+ for i := range ft.nodes {
+ n := ft.nodes[i]
+ if _, isID := idNodeTypes[n.Kind]; !isID {
+ continue
}
- nt := n.Type(lang)
- if _, isID := idNodeTypes[nt]; isID {
- name := n.Text(src)
- if len(name) >= minRefNameLength {
- if _, skip := skipNames[name]; !skip {
- // Skip if this identifier is the name child of a definition node.
- parent := n.Parent()
- if parent != nil {
- if _, isTarget := targetTypes[parent.Type(lang)]; isTarget {
- // Check if this is the first identifier child.
- // We use StartByte as a stable node identity (within one parse).
- nStart := n.StartByte()
- cnt := parent.ChildCount()
- for i := 0; i < cnt; i++ {
- child := parent.Child(i)
- if child == nil {
- continue
- }
- if _, childIsID := idNodeTypes[child.Type(lang)]; childIsID {
- if child.StartByte() == nStart {
- return // skip — it's a definition name
- }
- break
- }
- }
- }
- }
-
- line := int(n.StartPoint().Row) + 1
- col := int(n.StartPoint().Column)
- key := [3]any{name, line, col}
- if _, dup := seen[key]; !dup {
- seen[key] = struct{}{}
- refs = append(refs, Reference{
- Name: name,
- FilePath: filePath,
- Line: line,
- Col: col,
- Language: language,
- })
+ name := string(src[n.StartByte:n.EndByte])
+ if len(name) < minRefNameLength {
+ continue
+ }
+ if _, skip := skipNames[name]; skip {
+ continue
+ }
+ // Skip if this identifier is the name child of a definition node — i.e.
+ // the FIRST identifier child of a target (definition) parent.
+ if p := ft.parents[i]; p >= 0 {
+ if _, isTarget := targetTypes[ft.nodes[p].Kind]; isTarget {
+ isDefName := false
+ for _, c := range ft.children[p] {
+ if _, childIsID := idNodeTypes[ft.nodes[c].Kind]; childIsID {
+ isDefName = int(c) == i
+ break
}
}
+ if isDefName {
+ continue
+ }
}
- return // leaf — no children to recurse
}
- cnt := n.ChildCount()
- for i := 0; i < cnt; i++ {
- walk(n.Child(i))
+ line := int(n.StartRow) + 1
+ col := int(n.StartCol)
+ key := [3]any{name, line, col}
+ if _, dup := seen[key]; !dup {
+ seen[key] = struct{}{}
+ refs = append(refs, Reference{
+ Name: name,
+ FilePath: filePath,
+ Line: line,
+ Col: col,
+ Language: language,
+ })
}
}
- walk(root)
return refs
}
@@ -808,7 +844,7 @@ func extractReferences(
// Without these, the symbol_name field on the resulting chunk was nil and
// the CLI's `cix summary` rendered weird placeholders (`[method] bool`,
// `[function] `).
-func extractName(node *sitter.Node, lang *sitter.Language, src []byte) *string {
+func extractName(ft *flatTree, idx int, src []byte) *string {
nameTypes := map[string]struct{}{
"identifier": {},
"name": {},
@@ -819,14 +855,10 @@ func extractName(node *sitter.Node, lang *sitter.Language, src []byte) *string {
"simple_identifier": {},
"constant": {},
}
- cnt := node.ChildCount()
- for i := 0; i < cnt; i++ {
- child := node.Child(i)
- if child == nil {
- continue
- }
- if _, ok := nameTypes[child.Type(lang)]; ok {
- s := child.Text(src)
+ for _, c := range ft.children[idx] {
+ child := ft.nodes[c]
+ if _, ok := nameTypes[child.Kind]; ok {
+ s := string(src[child.StartByte:child.EndByte])
return &s
}
}
diff --git a/server/internal/chunker/chunker_test.go b/server/internal/chunker/chunker_test.go
index be5ab80..7c32c76 100644
--- a/server/internal/chunker/chunker_test.go
+++ b/server/internal/chunker/chunker_test.go
@@ -5,9 +5,25 @@ import (
"testing"
"time"
- sitter "github.com/odvcencio/gotreesitter"
+ "github.com/dvcdsys/code-index/server/internal/chunker/tswasm"
)
+// kindsOf parses src under the given language's grammar via the wasm backend and
+// returns the set of AST node-kind names present — the test helper that replaced
+// the old gotreesitter walk.
+func kindsOf(t *testing.T, lang, src string) map[string]struct{} {
+ t.Helper()
+ nodes, err := tswasm.ParseNodes("tree_sitter_"+lang, []byte(src))
+ if err != nil {
+ t.Fatalf("parse %q: %v", lang, err)
+ }
+ out := make(map[string]struct{}, len(nodes))
+ for _, n := range nodes {
+ out[n.Kind] = struct{}{}
+ }
+ return out
+}
+
func TestChunkFile_Python(t *testing.T) {
src := `def hello(name):
return "hello " + name
@@ -300,12 +316,12 @@ esac
t.Fatalf("unexpected error: %v", err)
}
- // Whether or not the guard fired on this synthetic input, total time
- // must stay under 2× parseBudget — otherwise the parser is running
- // uncapped.
- if elapsed > 2*parseBudget+500*time.Millisecond {
- t.Errorf("ChunkFile elapsed %s, expected < ~2× parseBudget (%s)",
- elapsed, parseBudget)
+ // The official tree-sitter (via wasm) has no catastrophic-backtracking
+ // pathology, so this gnarly bash should parse in well under a second; a
+ // generous absolute bound guards against any regression that hangs the
+ // indexer.
+ if elapsed > 5*time.Second {
+ t.Errorf("ChunkFile elapsed %s, expected well under 5s for official parser", elapsed)
}
if len(chunks) == 0 {
t.Error("expected at least one chunk (block or function), got 0")
@@ -584,7 +600,7 @@ contract C {
`
registryMu.RLock()
- fn, ok := languageRegistry["solidity"]
+ _, ok := languageRegistry["solidity"]
nodes := languageNodes["solidity"]
registryMu.RUnlock()
if !ok {
@@ -594,23 +610,7 @@ contract C {
t.Fatal("solidity has no node map")
}
- grammar := fn()
- if grammar == nil {
- t.Fatal("nil solidity grammar")
- }
-
- parser := sitter.NewParser(grammar)
- tree, err := parser.Parse([]byte(src))
- if err != nil {
- t.Fatalf("parse error: %v", err)
- }
- root := tree.RootNode()
- if root == nil {
- t.Fatal("nil root")
- }
-
- seen := map[string]struct{}{}
- collectNodeTypes(root, grammar, seen)
+ seen := kindsOf(t, "solidity", src)
for _, types := range nodes {
for _, ty := range types {
@@ -688,24 +688,24 @@ func chunkTypeCounts(chunks []Chunk) map[string]int {
return out
}
-// TestRegistry_AllFactoriesNonNil ensures every default-registered language
-// resolves to a usable *sitter.Language. A nil factory return would mean
-// gotreesitter renamed/removed a grammar between updates and we silently lost
-// support — better to fail loud here than at runtime in production.
-func TestRegistry_AllFactoriesNonNil(t *testing.T) {
+// TestRegistry_AllGrammarsPresent ensures every default-registered language has
+// its grammar exported by the wasm module. A missing export would mean a build
+// regression (grammar dropped from build.sh) and silent loss of support — better
+// to fail loud here than at runtime in production.
+func TestRegistry_AllGrammarsPresent(t *testing.T) {
defer Configure(nil)
Configure(nil)
for _, lang := range SupportedLanguages() {
t.Run(lang, func(t *testing.T) {
registryMu.RLock()
- fn := languageRegistry[lang]
+ export := languageRegistry[lang]
registryMu.RUnlock()
- if fn == nil {
- t.Fatalf("nil factory for %q", lang)
+ if export == "" {
+ t.Fatalf("no export mapping for %q", lang)
}
- if g := fn(); g == nil {
- t.Fatalf("factory returned nil grammar for %q", lang)
+ if !tswasm.HasLanguage(export) {
+ t.Fatalf("wasm module does not export %q for language %q", export, lang)
}
})
}
@@ -772,7 +772,7 @@ func TestRegistry_NodeNamesMatchAST(t *testing.T) {
for lang, src := range fixtures {
t.Run(lang, func(t *testing.T) {
registryMu.RLock()
- fn, regOK := languageRegistry[lang]
+ _, regOK := languageRegistry[lang]
nodes := languageNodes[lang]
registryMu.RUnlock()
@@ -783,21 +783,6 @@ func TestRegistry_NodeNamesMatchAST(t *testing.T) {
t.Skipf("%q has no node map (sliding-window only — by design)", lang)
}
- grammar := fn()
- if grammar == nil {
- t.Fatalf("nil grammar for %q", lang)
- }
-
- parser := sitter.NewParser(grammar)
- tree, err := parser.Parse([]byte(src))
- if err != nil {
- t.Fatalf("parse error for %q: %v", lang, err)
- }
- root := tree.RootNode()
- if root == nil {
- t.Fatalf("nil root for %q", lang)
- }
-
want := map[string]struct{}{}
for _, types := range nodes {
for _, ty := range types {
@@ -805,8 +790,7 @@ func TestRegistry_NodeNamesMatchAST(t *testing.T) {
}
}
- seen := map[string]struct{}{}
- collectNodeTypes(root, grammar, seen)
+ seen := kindsOf(t, lang, src)
matched := false
for ty := range want {
@@ -827,16 +811,6 @@ func TestRegistry_NodeNamesMatchAST(t *testing.T) {
}
}
-func collectNodeTypes(n *sitter.Node, lang *sitter.Language, out map[string]struct{}) {
- if n == nil {
- return
- }
- out[n.Type(lang)] = struct{}{}
- for i := 0; i < int(n.ChildCount()); i++ {
- collectNodeTypes(n.Child(i), lang, out)
- }
-}
-
func sampleKeys(m map[string]struct{}, n int) []string {
out := make([]string, 0, n)
for k := range m {
diff --git a/server/internal/chunker/comment_attach_test.go b/server/internal/chunker/comment_attach_test.go
new file mode 100644
index 0000000..14cb432
--- /dev/null
+++ b/server/internal/chunker/comment_attach_test.go
@@ -0,0 +1,131 @@
+package chunker
+
+import (
+ "strings"
+ "testing"
+)
+
+// TestLeadingCommentAttachment verifies that a declaration's doc comment is
+// pulled INTO the declaration's chunk instead of being stranded in the gap
+// between symbols (where the gap filler used to emit it as a standalone
+// micro "module" chunk — openapi.gen.go produced 377 such 60-byte chunks).
+func TestLeadingCommentAttachment(t *testing.T) {
+ src := `package m
+
+// Foo does foo things.
+// Second doc line.
+func Foo() int { return 1 }
+
+// Bar is a bar. (sibling of type_declaration, two levels above type_spec)
+type Bar struct{ X int }
+
+// Standalone banner — blank line below breaks adjacency.
+
+func Baz() int { return 2 }
+`
+ chunks, _, err := ChunkFile("/p/a.go", src, "go", 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ find := func(name string) *Chunk {
+ for i := range chunks {
+ if chunks[i].SymbolName != nil && *chunks[i].SymbolName == name {
+ return &chunks[i]
+ }
+ }
+ t.Fatalf("symbol %q not found in chunks", name)
+ return nil
+ }
+
+ foo := find("Foo")
+ if !strings.HasPrefix(foo.Content, "// Foo does foo things.") {
+ t.Errorf("Foo chunk must start with its doc comment, got %q", foo.Content)
+ }
+ if foo.SymbolSignature == nil || !strings.HasPrefix(*foo.SymbolSignature, "func Foo") {
+ t.Errorf("signature must stay the declaration line, got %v", foo.SymbolSignature)
+ }
+
+ // Doc comment of a type reaches the chunk through the type_declaration
+ // wrapper (the comment is NOT a direct sibling of type_spec).
+ bar := find("Bar")
+ if !strings.Contains(bar.Content, "// Bar is a bar.") {
+ t.Errorf("Bar chunk must contain its doc comment, got %q", bar.Content)
+ }
+
+ // A blank line between comment and declaration breaks the chain: the
+ // banner stays OUT of Baz's chunk (it remains module-gap content).
+ baz := find("Baz")
+ if strings.Contains(baz.Content, "Standalone banner") {
+ t.Errorf("banner comment separated by a blank line must not attach, got %q", baz.Content)
+ }
+
+ // And no gap chunk should consist of Foo's doc comment alone.
+ for _, c := range chunks {
+ if c.ChunkType == "module" && strings.TrimSpace(c.Content) == "// Foo does foo things.\n// Second doc line." {
+ t.Errorf("doc comment leaked into a standalone module chunk")
+ }
+ }
+}
+
+// TestLeadingCommentAttachment_Languages verifies the attachment works across
+// grammars — the mechanism is language-agnostic (comments carry tree-sitter's
+// "extra" flag in every grammar; declaration wrappers are climbed by
+// same-row), but each language nests declarations differently, so each gets a
+// smoke case: the doc comment must land inside a NON-module chunk.
+func TestLeadingCommentAttachment_Languages(t *testing.T) {
+ cases := []struct {
+ lang, path, src, marker string
+ }{
+ {
+ "typescript", "/p/a.ts",
+ "/** Greets the user. */\nexport function greet(name: string): string {\n return \"hi \" + name;\n}\n",
+ "/** Greets the user. */",
+ },
+ {
+ // C line + block comments; function_definition is a direct
+ // sibling of the comment.
+ "c", "/p/a.c",
+ "/* frobnicates the widget */\nint frob(void) { return 1; }\n",
+ "/* frobnicates the widget */",
+ },
+ {
+ // C struct behind a typedef: type_definition wraps
+ // struct_specifier — exercises the wrapper climb.
+ "c", "/p/b.c",
+ "/** Doxygen: widget state. */\ntypedef struct {\n int x;\n} widget_t;\n",
+ "/** Doxygen: widget state. */",
+ },
+ {
+ "python", "/p/a.py",
+ "# helper used by the frobnicator\ndef helper():\n return 1\n",
+ "# helper used by the frobnicator",
+ },
+ {
+ "rust", "/p/a.rs",
+ "/// Does the foo thing.\nfn foo() -> i32 { 1 }\n",
+ "/// Does the foo thing.",
+ },
+ {
+ "java", "/p/A.java",
+ "/** Java doc. */\npublic class A {\n void m() {}\n}\n",
+ "/** Java doc. */",
+ },
+ }
+ for _, tc := range cases {
+ chunks, _, err := ChunkFile(tc.path, tc.src, tc.lang, 0)
+ if err != nil {
+ t.Fatalf("%s: %v", tc.lang, err)
+ }
+ attached := false
+ for _, c := range chunks {
+ if c.ChunkType != "module" && c.ChunkType != "block" && strings.Contains(c.Content, tc.marker) {
+ attached = true
+ }
+ }
+ if !attached {
+ t.Errorf("%s (%s): doc comment %q not attached to a symbol chunk; chunks: %+v",
+ tc.lang, tc.path, tc.marker, chunks)
+ }
+ }
+}
diff --git a/server/internal/chunker/memstress_test.go b/server/internal/chunker/memstress_test.go
new file mode 100644
index 0000000..895a159
--- /dev/null
+++ b/server/internal/chunker/memstress_test.go
@@ -0,0 +1,123 @@
+//go:build unix
+
+package chunker
+
+// Temporary memory-stress harness for the wasm tree-sitter backend.
+// Run explicitly:
+// go test ./internal/chunker -run TestMemStress -v -timeout 30m
+// Not part of normal CI (guarded by CIX_MEMSTRESS).
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "syscall"
+ "testing"
+
+ "github.com/dvcdsys/code-index/server/internal/chunker/tswasm"
+)
+
+func langForExt(path string) string {
+ switch strings.ToLower(filepath.Ext(path)) {
+ case ".go":
+ return "go"
+ case ".ts":
+ return "typescript"
+ case ".tsx":
+ return "tsx"
+ case ".js":
+ return "javascript"
+ case ".py":
+ return "python"
+ case ".md":
+ return "markdown"
+ case ".css":
+ return "css"
+ case ".sh":
+ return "bash"
+ case ".c", ".h":
+ return "c"
+ default:
+ return ""
+ }
+}
+
+func rssMB() float64 {
+ var ru syscall.Rusage
+ syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
+ // darwin: bytes; linux: KiB
+ if runtime.GOOS == "darwin" {
+ return float64(ru.Maxrss) / (1 << 20)
+ }
+ return float64(ru.Maxrss) / 1024
+}
+
+func TestMemStress(t *testing.T) {
+ if os.Getenv("CIX_MEMSTRESS") == "" {
+ t.Skip("set CIX_MEMSTRESS=1 to run")
+ }
+ root := os.Getenv("CIX_MEMSTRESS_ROOT")
+ if root == "" {
+ root = "../../.."
+ }
+
+ type f struct {
+ path, lang, content string
+ }
+ var files []f
+ filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if d.IsDir() {
+ name := d.Name()
+ if name == ".git" || name == "node_modules" || name == "dist" || name == ".venv" {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ lang := langForExt(p)
+ if lang == "" {
+ return nil
+ }
+ b, err := os.ReadFile(p)
+ if err != nil || len(b) == 0 || len(b) > 512*1024 {
+ return nil
+ }
+ files = append(files, f{p, lang, string(b)})
+ return nil
+ })
+ t.Logf("collected %d files from %s", len(files), root)
+
+ report := func(tag string, n int) {
+ runtime.GC()
+ var ms runtime.MemStats
+ runtime.ReadMemStats(&ms)
+ ps := tswasm.PoolStats()
+ t.Logf("%s n=%d heapAlloc=%.1fMB heapSys=%.1fMB sys=%.1fMB numGC=%d maxRSS=%.1fMB | pool: created=%d closed=%d recycled=%d live=%d idle=%d idleMem=%.1fMB",
+ tag, n,
+ float64(ms.HeapAlloc)/(1<<20), float64(ms.HeapSys)/(1<<20), float64(ms.Sys)/(1<<20), ms.NumGC, rssMB(),
+ ps.Created, ps.Closed, ps.Recycled, ps.LiveOpen, ps.IdlePooled, float64(ps.IdleMemBytes)/(1<<20))
+ }
+
+ report("start", 0)
+ total := 0
+ for pass := 1; pass <= 4; pass++ {
+ for _, fl := range files {
+ chunks, refs, err := ChunkFile(fl.path, fl.content, fl.lang, 0)
+ if err != nil {
+ t.Fatalf("chunk %s: %v", fl.path, err)
+ }
+ _ = chunks
+ _ = refs
+ total++
+ if total%500 == 0 {
+ report(" tick", total)
+ }
+ }
+ report(fmt.Sprintf("pass %d done", pass), total)
+ }
+ report("end", total)
+}
diff --git a/server/internal/chunker/minified_test.go b/server/internal/chunker/minified_test.go
new file mode 100644
index 0000000..ced3713
--- /dev/null
+++ b/server/internal/chunker/minified_test.go
@@ -0,0 +1,52 @@
+package chunker
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestLooksMinified(t *testing.T) {
+ longLine := strings.Repeat("var a=1;", 600) // ~4800 chars, one line
+ cases := []struct {
+ name string
+ path string
+ content string
+ language string
+ want bool
+ }{
+ {"min.js by name", "/p/vendor/jquery.min.js", "var a=1;\n", "javascript", true},
+ {"min.css by name", "/p/static/site.min.css", "body{}\n", "css", true},
+ {"bundle.js by name", "/p/dist/app.bundle.js", "var a=1;\n", "javascript", true},
+ {"long single line js", "/p/dist/out.js", longLine, "javascript", true},
+ {"long line ts no trailing newline", "/p/gen/types.ts", "const x = 1;\n" + longLine, "typescript", true},
+ {"normal ts", "/p/src/app.ts", "export function f(): number { return 1; }\n", "typescript", false},
+ {"long line in go ignored", "/p/gen/openapi.gen.go", longLine, "go", false},
+ {"normal css", "/p/src/site.css", ".a { color: red; }\n", "css", false},
+ }
+ for _, tc := range cases {
+ if got := looksMinified(tc.path, tc.content, tc.language); got != tc.want {
+ t.Errorf("%s: looksMinified = %v, want %v", tc.name, got, tc.want)
+ }
+ }
+}
+
+// TestChunkFile_MinifiedFallsBack verifies minified input bypasses the AST path
+// entirely and is still indexed via sliding-window "block" chunks.
+func TestChunkFile_MinifiedFallsBack(t *testing.T) {
+ content := strings.Repeat("function f(){return 1};", 300) // one ~7 KB line
+ chunks, refs, err := ChunkFile("/p/dist/app.min.js", content, "javascript", 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(refs) != 0 {
+ t.Errorf("expected no references for minified file, got %d", len(refs))
+ }
+ if len(chunks) == 0 {
+ t.Fatal("minified file must still produce sliding-window chunks")
+ }
+ for _, c := range chunks {
+ if c.ChunkType != "block" {
+ t.Errorf("expected only block chunks, got %q", c.ChunkType)
+ }
+ }
+}
diff --git a/server/internal/chunker/tswasm/.gitignore b/server/internal/chunker/tswasm/.gitignore
new file mode 100644
index 0000000..d0bc66f
--- /dev/null
+++ b/server/internal/chunker/tswasm/.gitignore
@@ -0,0 +1,4 @@
+# Raw wasm is a build intermediate produced by build.sh; only the brotli-
+# compressed ts-core.wasm.br (~3 MB) is committed and embedded. Regenerate the
+# raw blob with ./build.sh (needs zig).
+ts-core.wasm
diff --git a/server/internal/chunker/tswasm/build.sh b/server/internal/chunker/tswasm/build.sh
new file mode 100755
index 0000000..8f5774b
--- /dev/null
+++ b/server/internal/chunker/tswasm/build.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+# Builds ts-core.wasm: the OFFICIAL tree-sitter C runtime + the base grammars +
+# our host_extra.c (the batched ts_dump_tree walk), compiled to ONE standalone
+# wasm32-wasi reactor module via `zig cc`. No emscripten, no JS glue.
+#
+# Requires: zig (clang + wasi-libc cross-compile), git, and tree-sitter CLI (only
+# for grammars whose repo ships no committed parser.c — gen=1 rows).
+#
+# For wasm we compile each grammar IN PLACE from a full clone, so relative
+# includes (e.g. typescript's ../../common/scanner.h) and src-root headers (html
+# tag.h, haskell unicode.h) resolve naturally — none of the vendor.sh copy/rewrite
+# dance is needed. Quirks that remain: SHA pins (dart), `tree-sitter generate`
+# (sql), and a 2nd grammar from one repo (tsx). See plan §6.1.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+TS_VERSION="${TS_VERSION:-v0.25.10}"
+OUT="${OUT:-ts-core.wasm}"
+WORK="$(mktemp -d)"
+trap 'rm -rf "$WORK"' EXIT
+
+# id repo ref srcsubdir [gen]
+GRAMMARS=(
+ "python tree-sitter/tree-sitter-python v0.25.0 src"
+ "typescript tree-sitter/tree-sitter-typescript v0.23.2 typescript/src"
+ "tsx tree-sitter/tree-sitter-typescript v0.23.2 tsx/src"
+ "javascript tree-sitter/tree-sitter-javascript v0.25.0 src"
+ "go tree-sitter/tree-sitter-go v0.25.0 src"
+ "rust tree-sitter/tree-sitter-rust v0.24.2 src"
+ "java tree-sitter/tree-sitter-java v0.23.5 src"
+ "c tree-sitter/tree-sitter-c v0.24.2 src"
+ "cpp tree-sitter/tree-sitter-cpp v0.23.4 src"
+ "ruby tree-sitter/tree-sitter-ruby v0.23.1 src"
+ "c_sharp tree-sitter/tree-sitter-c-sharp v0.23.5 src"
+ "php tree-sitter/tree-sitter-php v0.24.2 php/src"
+ "swift alex-pinkus/tree-sitter-swift 0.7.3-with-generated-files src"
+ "kotlin tree-sitter-grammars/tree-sitter-kotlin v1.1.0 src"
+ "scala tree-sitter/tree-sitter-scala v0.26.0 src"
+ "bash tree-sitter/tree-sitter-bash v0.25.1 src"
+ "lua tree-sitter-grammars/tree-sitter-lua v0.5.0 src"
+ "dart UserNobody14/tree-sitter-dart a9bdfa3 src"
+ "r r-lib/tree-sitter-r v1.2.0 src"
+ "objc tree-sitter-grammars/tree-sitter-objc v3.0.2 src"
+ "html tree-sitter/tree-sitter-html v0.23.2 src"
+ "css tree-sitter/tree-sitter-css v0.25.0 src"
+ "scss tree-sitter-grammars/tree-sitter-scss v1.0.0 src"
+ "sql DerekStride/tree-sitter-sql v0.3.11 src 1"
+ "markdown tree-sitter-grammars/tree-sitter-markdown v0.5.3 tree-sitter-markdown/src"
+ "zig tree-sitter-grammars/tree-sitter-zig v1.1.2 src"
+ "julia tree-sitter/tree-sitter-julia v0.25.0 src"
+ "fortran stadelmanma/tree-sitter-fortran v0.6.0 src"
+ "haskell tree-sitter/tree-sitter-haskell v0.23.1 src"
+ "ocaml tree-sitter/tree-sitter-ocaml v0.25.0 grammars/ocaml/src"
+ "solidity JoranHonig/tree-sitter-solidity v1.2.13 src"
+)
+
+clone() { # repo ref dest — tag/branch fast path, SHA fallback
+ local repo="$1" ref="$2" dest="$3"
+ git clone --depth 1 --branch "$ref" "https://github.com/$repo" "$dest" >/dev/null 2>&1 && return 0
+ git clone "https://github.com/$repo" "$dest" >/dev/null 2>&1 || return 1
+ git -C "$dest" checkout "$ref" >/dev/null 2>&1
+}
+
+echo "→ tree-sitter runtime $TS_VERSION"
+git clone --depth 1 --branch "$TS_VERSION" https://github.com/tree-sitter/tree-sitter "$WORK/tree-sitter" 2>/dev/null
+
+SRCS=( "$WORK/tree-sitter/lib/src/lib.c" "csrc/host_extra.c" )
+INCS=( -I "$WORK/tree-sitter/lib/include" -I "$WORK/tree-sitter/lib/src" )
+EXPORTS=()
+BUILT=() ; FAILED=()
+
+for row in "${GRAMMARS[@]}"; do
+ read -r id repo ref sub gen <<<"$row"
+ printf ' %-12s %s@%s ' "$id" "$repo" "$ref"
+ if ! clone "$repo" "$ref" "$WORK/$id"; then echo "CLONE FAIL"; FAILED+=("$id"); continue; fi
+ gsrc="$WORK/$id/$sub"
+ if [ "${gen:-0}" = "1" ] && [ ! -f "$gsrc/parser.c" ]; then
+ ( cd "$WORK/$id" && tree-sitter generate >/dev/null 2>&1 ) || true
+ fi
+ if [ ! -f "$gsrc/parser.c" ]; then echo "NO parser.c"; FAILED+=("$id"); continue; fi
+ SRCS+=( "$gsrc/parser.c" )
+ [ -f "$gsrc/scanner.c" ] && SRCS+=( "$gsrc/scanner.c" )
+ [ -f "$gsrc/scanner.cc" ] && SRCS+=( "$gsrc/scanner.cc" )
+ INCS+=( -I "$gsrc" )
+ EXPORTS+=( -Wl,--export=tree_sitter_$id )
+ BUILT+=("$id")
+ echo "ok"
+done
+
+echo "→ compiling ${#SRCS[@]} sources, ${#BUILT[@]} grammars → $OUT"
+zig cc --target=wasm32-wasi-musl -mexec-model=reactor \
+ "${INCS[@]}" "${SRCS[@]}" \
+ -o "$OUT" -Oz -fPIC -Wl,--no-entry -Wl,--strip-debug \
+ -Wl,--export=malloc -Wl,--export=free \
+ -Wl,--export=ts_parser_new -Wl,--export=ts_parser_delete \
+ -Wl,--export=ts_parser_set_language -Wl,--export=ts_parser_parse_string \
+ -Wl,--export=ts_parser_reset \
+ -Wl,--export=ts_tree_delete -Wl,--export=ts_tree_root_node \
+ -Wl,--export=ts_node_child_count -Wl,--export=ts_node_child \
+ -Wl,--export=ts_node_type -Wl,--export=ts_node_start_byte \
+ -Wl,--export=ts_node_end_byte -Wl,--export=ts_node_has_error \
+ -Wl,--export=ts_dump_tree -Wl,--export=ts_dump_rec_size \
+ -Wl,--export=ts_language_symbol_count -Wl,--export=ts_language_symbol_name \
+ "${EXPORTS[@]}"
+
+echo "built $OUT ($(du -h "$OUT" | cut -f1)) — runtime $TS_VERSION, ${#BUILT[@]} grammars"
+[ ${#FAILED[@]} -gt 0 ] && echo "FAILED: ${FAILED[*]}"
+echo "grammars: ${BUILT[*]}"
+
+# Brotli-compress to the committed artifact the package embeds. The raw .wasm is
+# a gitignored intermediate; only ts-core.wasm.br (~3 MB) goes into git.
+echo "→ brotli-compressing → ${OUT}.br"
+go run compress.go
diff --git a/server/internal/chunker/tswasm/compress.go b/server/internal/chunker/tswasm/compress.go
new file mode 100644
index 0000000..dcb8c15
--- /dev/null
+++ b/server/internal/chunker/tswasm/compress.go
@@ -0,0 +1,39 @@
+//go:build ignore
+
+// compress.go brotli-compresses the raw ts-core.wasm (produced by build.sh) into
+// the committed ts-core.wasm.br that tswasm.go embeds. The raw .wasm is a build
+// intermediate (gitignored); only the ~10x-smaller .br is committed, so a server
+// build needs neither zig nor the 56 MB blob in git.
+//
+// go run compress.go # invoked as the last step of build.sh
+package main
+
+import (
+ "log"
+ "os"
+
+ "github.com/andybalholm/brotli"
+)
+
+func main() {
+ raw, err := os.ReadFile("ts-core.wasm")
+ if err != nil {
+ log.Fatalf("read ts-core.wasm (run build.sh first): %v", err)
+ }
+ out, err := os.Create("ts-core.wasm.br")
+ if err != nil {
+ log.Fatal(err)
+ }
+ w := brotli.NewWriterLevel(out, brotli.BestCompression)
+ if _, err := w.Write(raw); err != nil {
+ log.Fatal(err)
+ }
+ if err := w.Close(); err != nil {
+ log.Fatal(err)
+ }
+ if err := out.Close(); err != nil {
+ log.Fatal(err)
+ }
+ fi, _ := os.Stat("ts-core.wasm.br")
+ log.Printf("compressed %d → ts-core.wasm.br (%d bytes)", len(raw), fi.Size())
+}
diff --git a/server/internal/chunker/tswasm/csrc/host_extra.c b/server/internal/chunker/tswasm/csrc/host_extra.c
new file mode 100644
index 0000000..42cec34
--- /dev/null
+++ b/server/internal/chunker/tswasm/csrc/host_extra.c
@@ -0,0 +1,75 @@
+// host_extra.c — custom exports compiled INTO the wasm module alongside the
+// official tree-sitter runtime. The whole point is `ts_dump_tree`: it walks the
+// parsed tree ENTIRELY inside the guest and writes a flat pre-order array of
+// fixed-size records into linear memory in ONE shot. The host then does a single
+// Memory.Read and runs the chunker's node matching in pure Go — turning the
+// ~3 wazero calls-per-node of the naive walk into ~1 call per parse.
+//
+// See docs/wasm-treesitter-implementation-plan.md §7.2 / §7.3.
+#include "tree_sitter/api.h"
+#include
+
+// One record per node. ALL fields uint32 so the Go side reads 9 little-endian
+// uint32s with zero packing ambiguity (recSize = 36). kind_id is the TSSymbol
+// (fits in 16 bits; widened for clean alignment). The host resolves kind_id ->
+// kind-name once per language via ts_language_symbol_name (see below), so the
+// per-node string lookup never crosses the boundary.
+typedef struct {
+ uint32_t kind_id; // ts_node_symbol
+ uint32_t start_byte;
+ uint32_t end_byte;
+ uint32_t start_row; // ts_node_start_point().row (0-based line)
+ uint32_t start_col; // ts_node_start_point().column
+ uint32_t end_row; // ts_node_end_point().row
+ uint32_t end_col; // ts_node_end_point().column
+ uint32_t depth; // pre-order depth (parent reconstruction via depth stack)
+ uint32_t flags; // bit0 named, bit1 error, bit2 missing, bit3 extra
+} NodeRec;
+
+static void emit(NodeRec *out, uint32_t i, TSNode n, uint32_t depth) {
+ out[i].kind_id = ts_node_symbol(n);
+ out[i].start_byte = ts_node_start_byte(n);
+ out[i].end_byte = ts_node_end_byte(n);
+ TSPoint sp = ts_node_start_point(n);
+ out[i].start_row = sp.row;
+ out[i].start_col = sp.column;
+ TSPoint ep = ts_node_end_point(n);
+ out[i].end_row = ep.row;
+ out[i].end_col = ep.column;
+ out[i].depth = depth;
+ uint32_t f = 0;
+ if (ts_node_is_named(n)) f |= 1u;
+ if (ts_node_is_error(n)) f |= 2u;
+ if (ts_node_is_missing(n)) f |= 4u;
+ if (ts_node_is_extra(n)) f |= 8u;
+ out[i].flags = f;
+}
+
+// ts_dump_tree writes up to `cap` records and returns the TRUE node count. If the
+// return value > cap the buffer was too small: the host re-mallocs to the
+// returned count and calls again. Iterative pre-order DFS via a tree cursor — no
+// recursion, no per-node malloc, no boundary crossings.
+uint32_t ts_dump_tree(const TSTree *tree, NodeRec *out, uint32_t cap) {
+ if (tree == 0) return 0;
+ TSTreeCursor cur = ts_tree_cursor_new(ts_tree_root_node(tree));
+ uint32_t count = 0;
+ uint32_t depth = 0;
+ for (;;) {
+ if (count < cap) {
+ emit(out, count, ts_tree_cursor_current_node(&cur), depth);
+ }
+ count++;
+ if (ts_tree_cursor_goto_first_child(&cur)) { depth++; continue; }
+ for (;;) {
+ if (ts_tree_cursor_goto_next_sibling(&cur)) break;
+ if (!ts_tree_cursor_goto_parent(&cur)) {
+ ts_tree_cursor_delete(&cur);
+ return count;
+ }
+ depth--;
+ }
+ }
+}
+
+// recSize lets the host assert its struct layout matches the guest's.
+uint32_t ts_dump_rec_size(void) { return (uint32_t)sizeof(NodeRec); }
diff --git a/server/internal/chunker/tswasm/memprofile_test.go b/server/internal/chunker/tswasm/memprofile_test.go
new file mode 100644
index 0000000..4a5d750
--- /dev/null
+++ b/server/internal/chunker/tswasm/memprofile_test.go
@@ -0,0 +1,156 @@
+//go:build unix
+
+package tswasm
+
+// Temporary breakdown harness: where does the memory go?
+// Run: CIX_MEMSTRESS=1 go test ./internal/chunker/tswasm -run TestMemBreakdown -v
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ goruntime "runtime"
+ "strings"
+ "sync"
+ "syscall"
+ "testing"
+
+ "github.com/andybalholm/brotli"
+)
+
+func report(t *testing.T, tag string) {
+ goruntime.GC()
+ var ms goruntime.MemStats
+ goruntime.ReadMemStats(&ms)
+ var ru syscall.Rusage
+ syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
+ rss := float64(ru.Maxrss) / (1 << 20)
+ if goruntime.GOOS != "darwin" {
+ rss = float64(ru.Maxrss) / 1024
+ }
+ ps := PoolStats()
+ t.Logf("%-28s heapAlloc=%7.1fMB heapSys=%7.1fMB maxRSS=%7.1fMB | created=%d closed=%d recycled=%d live=%d idle=%d idleMem=%.1fMB",
+ tag, float64(ms.HeapAlloc)/(1<<20), float64(ms.HeapSys)/(1<<20), rss,
+ ps.Created, ps.Closed, ps.Recycled, ps.LiveOpen, ps.IdlePooled, float64(ps.IdleMemBytes)/(1<<20))
+}
+
+func TestMemBreakdown(t *testing.T) {
+ if os.Getenv("CIX_MEMSTRESS") == "" {
+ t.Skip("set CIX_MEMSTRESS=1 to run")
+ }
+ report(t, "start")
+
+ // 1. decompress only
+ raw, err := io.ReadAll(brotli.NewReader(bytes.NewReader(wasmBrotli)))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("raw wasm: %.1fMB", float64(len(raw))/(1<<20))
+ report(t, "after decompress")
+ raw = nil
+ report(t, "after drop raw")
+
+ // 2. compile (via initRuntime)
+ rtOnce.Do(initRuntime)
+ if rtErr != nil {
+ t.Fatal(rtErr)
+ }
+ report(t, "after CompileModule")
+
+ // 3. one instance
+ e, err := newEngine()
+ if err != nil {
+ t.Fatal(err)
+ }
+ report(t, "after 1 instance")
+
+ // 4. parse a small file
+ if _, err := e.parseNodes("tree_sitter_go", []byte("package m\nfunc F(){}\n")); err != nil {
+ t.Fatal(err)
+ }
+ report(t, "after small parse")
+
+ // 5. parse a big file (~500KB Go) — near the indexer's maxContentBytes cap
+ var sb strings.Builder
+ sb.WriteString("package m\n")
+ for i := 0; sb.Len() < 500*1024; i++ {
+ fmt.Fprintf(&sb, "func F%d(a, b int) int { x := a + b; y := x * 2; s := fmt.Sprintf(\"%%d\", y); _ = s; return y }\n", i)
+ }
+ big := []byte(sb.String())
+ nodes, err := e.parseNodes("tree_sitter_go", big)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("big parse: %d nodes from %dKB", len(nodes), len(big)/1024)
+ report(t, "after big parse (go)")
+
+ // repeat big parse 10x on same instance — does instance memory keep growing?
+ for range 10 {
+ if _, err := e.parseNodes("tree_sitter_go", big); err != nil {
+ t.Fatal(err)
+ }
+ }
+ t.Logf("instance memSize now: %.1fMB (baseline %.1fMB)", float64(e.memSize())/(1<<20), float64(baselineBytes.Load())/(1<<20))
+ report(t, "after 10 more big parses")
+ e.close()
+ report(t, "after close instance")
+
+ // 6. concurrency 3 with big typescript-ish files through the public API
+ tsSrc := []byte(strings.Repeat("export function foo(a: number, b: string): Promise { const x = a + b.length; if (x > 0) { console.log(x); } }\nclass Bar { private x: number = 1; method(y: number): number { return this.x + y; } }\n", 1500))
+ t.Logf("ts src: %dKB", len(tsSrc)/1024)
+ var wg sync.WaitGroup
+ for g := 0; g < 3; g++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for range 20 {
+ if _, err := ParseNodes("tree_sitter_typescript", tsSrc); err != nil {
+ t.Errorf("ts parse: %v", err)
+ return
+ }
+ }
+ }()
+ }
+ wg.Wait()
+ report(t, "after 3x20 concurrent big TS")
+
+ // 7. churn: force recycling (big parses with low threshold)
+ oldRec := RecycleGrowthBytes
+ RecycleGrowthBytes = 8 << 20
+ for range 10 {
+ if _, err := ParseNodes("tree_sitter_go", big); err != nil {
+ t.Fatal(err)
+ }
+ }
+ RecycleGrowthBytes = oldRec
+ report(t, "after 10 recycle churns")
+}
+
+// TestChurnPlateau: does sustained recycle churn plateau or grow unboundedly?
+func TestChurnPlateau(t *testing.T) {
+ if os.Getenv("CIX_MEMSTRESS") == "" {
+ t.Skip("set CIX_MEMSTRESS=1 to run")
+ }
+ var sb strings.Builder
+ sb.WriteString("package m\n")
+ for i := 0; sb.Len() < 500*1024; i++ {
+ fmt.Fprintf(&sb, "func F%d(a, b int) int { x := a + b; y := x * 2; s := fmt.Sprintf(\"%%d\", y); _ = s; return y }\n", i)
+ }
+ big := []byte(sb.String())
+
+ oldRec := RecycleGrowthBytes
+ RecycleGrowthBytes = 8 << 20 // force recycle every big parse
+ defer func() { RecycleGrowthBytes = oldRec }()
+
+ report(t, "churn start")
+ for i := 1; i <= 100; i++ {
+ if _, err := ParseNodes("tree_sitter_go", big); err != nil {
+ t.Fatal(err)
+ }
+ if i%20 == 0 {
+ report(t, fmt.Sprintf("churn %d", i))
+ }
+ }
+ report(t, "churn end")
+}
diff --git a/server/internal/chunker/tswasm/mmap_other.go b/server/internal/chunker/tswasm/mmap_other.go
new file mode 100644
index 0000000..48a717b
--- /dev/null
+++ b/server/internal/chunker/tswasm/mmap_other.go
@@ -0,0 +1,10 @@
+//go:build !unix
+
+package tswasm
+
+import "github.com/tetratelabs/wazero/experimental"
+
+// linearMemoryAllocator returns nil on non-unix platforms (no syscall.Mmap):
+// initRuntime falls back to wazero's default Go-heap allocator. Functionally
+// identical, just with the worse memory profile documented in mmap_unix.go.
+func linearMemoryAllocator() experimental.MemoryAllocator { return nil }
diff --git a/server/internal/chunker/tswasm/mmap_unix.go b/server/internal/chunker/tswasm/mmap_unix.go
new file mode 100644
index 0000000..14d1e5d
--- /dev/null
+++ b/server/internal/chunker/tswasm/mmap_unix.go
@@ -0,0 +1,79 @@
+//go:build unix
+
+package tswasm
+
+import (
+ "syscall"
+
+ "github.com/tetratelabs/wazero/experimental"
+)
+
+// mmapMemory backs a wasm linear memory with an anonymous mmap mapping instead
+// of wazero's default Go-heap []byte. Two wins, both measured on the chunker
+// churn workload (see memprofile_test.go):
+//
+// - Grow is free. The full max region (MemLimitPages) is reserved up-front —
+// virtual only; the OS commits pages on first touch — so Reallocate just
+// re-slices. The default allocator grows by append-realloc-copy, which
+// turned ONE big parse into 100s of MB of transient Go-heap garbage and
+// ballooned heapSys to ~1.5 GiB.
+// - Free munmaps. A closed/recycled instance's memory leaves RSS immediately,
+// instead of lingering in the Go heap until the scavenger returns it.
+//
+// Not safe for wasm shared memories (the backing address never moves here, but
+// nothing in this package uses threads/shared memory anyway).
+type mmapMemory struct {
+ // full is the original full-length mapping. Munmap must be called with a
+ // slice whose len equals the mapping size — Reallocate hands out shorter
+ // re-slices, so the original is kept separately.
+ full []byte
+}
+
+func (m *mmapMemory) Reallocate(size uint64) []byte {
+ if m.full == nil || size > uint64(len(m.full)) {
+ // Over max → wasm memory.grow fails → guest malloc returns NULL →
+ // tree-sitter aborts → contained trap → sliding-window fallback.
+ return nil
+ }
+ return m.full[:size:len(m.full)]
+}
+
+func (m *mmapMemory) Free() {
+ if m.full != nil {
+ _ = syscall.Munmap(m.full)
+ m.full = nil
+ }
+}
+
+// heapMemory is the fallback when mmap fails (ENOMEM, exotic platforms): a
+// fixed-capacity Go-heap buffer. cap is reserved once at max (virtual for large
+// allocations — the Go runtime mmaps big spans and commits on touch), so grow
+// is still a re-slice with no realloc-copy; the OS-return-on-Free win is lost.
+type heapMemory struct {
+ buf []byte // len = current size, cap = max, never reallocated
+}
+
+func (h *heapMemory) Reallocate(size uint64) []byte {
+ if size > uint64(cap(h.buf)) {
+ return nil
+ }
+ h.buf = h.buf[:size]
+ return h.buf
+}
+
+func (h *heapMemory) Free() { h.buf = nil }
+
+// linearMemoryAllocator returns the mmap-backed allocator. A nil return (only
+// on non-unix builds, see mmap_other.go) makes initRuntime fall back to
+// wazero's default Go-heap allocator.
+func linearMemoryAllocator() experimental.MemoryAllocator {
+ return experimental.MemoryAllocatorFunc(func(_, max uint64) experimental.LinearMemory {
+ buf, err := syscall.Mmap(-1, 0, int(max),
+ syscall.PROT_READ|syscall.PROT_WRITE,
+ syscall.MAP_ANON|syscall.MAP_PRIVATE)
+ if err != nil {
+ return &heapMemory{buf: make([]byte, 0, max)}
+ }
+ return &mmapMemory{full: buf}
+ })
+}
diff --git a/server/internal/chunker/tswasm/ts-core.wasm.br b/server/internal/chunker/tswasm/ts-core.wasm.br
new file mode 100644
index 0000000..173501e
Binary files /dev/null and b/server/internal/chunker/tswasm/ts-core.wasm.br differ
diff --git a/server/internal/chunker/tswasm/tswasm.go b/server/internal/chunker/tswasm/tswasm.go
new file mode 100644
index 0000000..7f8d3ca
--- /dev/null
+++ b/server/internal/chunker/tswasm/tswasm.go
@@ -0,0 +1,560 @@
+// Package tswasm is the pure-Go tree-sitter backend for the chunker: the
+// official tree-sitter C runtime + the base grammars + a batched-walk export
+// (csrc/host_extra.c), compiled to one standalone wasm32-wasi module
+// (ts-core.wasm via build.sh) and driven through wazero — no cgo, no JS.
+//
+// Parse the source, then ts_dump_tree walks the WHOLE tree inside the guest and
+// writes a flat pre-order []NodeRec into linear memory; the host does ONE
+// Memory.Read and decodes it. kind_id (TSSymbol) is resolved to a kind name once
+// per language via ts_language_symbol_name (cached). This avoids the
+// ~3-wazero-calls-per-node of a naive walk.
+//
+// Concurrency: a wazero module instance shares one linear memory and is NOT safe
+// for concurrent calls, so each parse borrows an *engine from a pool (one wazero
+// instance, its own memory) and returns it. The compiled module is shared.
+package tswasm
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "sync"
+ "sync/atomic"
+
+ "github.com/andybalholm/brotli"
+ "github.com/tetratelabs/wazero"
+ "github.com/tetratelabs/wazero/api"
+ "github.com/tetratelabs/wazero/experimental"
+ "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
+)
+
+// ts-core.wasm.br is the brotli-compressed module (~3 MB committed vs ~56 MB raw;
+// the raw .wasm is a gitignored build intermediate). Decompressed once at startup.
+//
+//go:embed ts-core.wasm.br
+var wasmBrotli []byte
+
+// recSize must match sizeof(NodeRec) in csrc/host_extra.c (9 × uint32). Asserted
+// against ts_dump_rec_size() when the first engine is created.
+const recSize = 36
+
+// Memory/pool knobs. wasm linear memory only GROWS within an instance (never
+// shrinks), so the strategy is: cap each instance, recycle (close) instances
+// whose memory grew past a threshold, and bound how many idle instances we
+// retain. Set these BEFORE the first ParseNodes call (the server wires CIX_*
+// env vars to them at startup).
+// IMPORTANT: each instance loads ALL 31 grammar tables into its OWN linear
+// memory, so the BASELINE is ~69 MiB per instance — the dominant memory cost,
+// multiplied by how many instances are LIVE concurrently. (Unlike gotreesitter,
+// where the tables were shared Go data.) The fix is MaxConcurrentInstances: a
+// hard semaphore that DECOUPLES chunker parallelism from the indexer's embedding
+// concurrency, so peak chunker memory ≈ MaxConcurrentInstances × ~69 MiB no
+// matter how many files the indexer embeds in parallel. (A future per-grammar-
+// module split, plan §10.5, would cut the baseline to a few MiB.)
+var (
+ // MaxConcurrentInstances is the hard cap on engine instances in use at once —
+ // the chunker's OWN concurrency, independent of CIX_MAX_EMBEDDING_CONCURRENCY.
+ // ParseNodes blocks (briefly — a parse is ~ms) until a slot frees. This is
+ // what stops a high embedding concurrency from spawning N×69 MiB chunker
+ // instances and OOM-ing. Bounds peak chunker memory ≈ this × per-instance.
+ // Set BEFORE the first ParseNodes call. 0 → 3.
+ MaxConcurrentInstances = 3
+
+ // MemLimitPages caps each instance's linear memory (64 KiB pages); an
+ // over-limit parse traps → sliding-window fallback, never a host OOM. Must be
+ // well above the ~69 MiB baseline. 4096 pages = 256 MiB: the indexer caps
+ // file content at 512 KiB and the worst measured instance (500 KiB
+ // pathological Go, 358k nodes) reached 130 MiB total, so 256 MiB is ~2×
+ // headroom; it is also the size of the virtual region the mmap allocator
+ // reserves per instance. 0 = wazero default (4 GiB).
+ MemLimitPages uint32 = 4096
+
+ // RecycleGrowthBytes: a released instance whose memory grew more than this
+ // ABOVE the baseline (i.e. it parsed a huge file) is CLOSED instead of pooled,
+ // so one pathological file doesn't permanently inflate a pooled instance.
+ // A fresh instance returns to the ~69 MiB baseline. 0 disables.
+ RecycleGrowthBytes uint64 = 128 << 20 // 128 MiB over baseline
+
+ // MaxIdleInstances bounds idle instances kept in the pool. The indexer
+ // chunks files SEQUENTIALLY within a session, so steady state is one
+ // instance; keeping just 1 idle avoids 2 more ~69-130 MiB instances
+ // surviving a brief concurrency burst forever. Trade-off: under SUSTAINED
+ // concurrent sessions (several projects indexing in parallel for long
+ // stretches) the over-cap engines churn close/create on each release —
+ // bump CIX_CHUNK_MAX_IDLE if that's your profile. 0 → matches
+ // MaxConcurrentInstances.
+ MaxIdleInstances = 1
+)
+
+// concLimiter is a RESIZABLE concurrency limiter (a fixed channel can't be
+// resized, and we want live tuning from the dashboard). acquire blocks until
+// inUse < max; setMax changes the ceiling and wakes waiters to re-check.
+type concLimiter struct {
+ mu sync.Mutex
+ cond *sync.Cond
+ inUse int
+ max int
+}
+
+func newConcLimiter(max int) *concLimiter {
+ if max < 1 {
+ max = 1
+ }
+ l := &concLimiter{max: max}
+ l.cond = sync.NewCond(&l.mu)
+ return l
+}
+
+func (l *concLimiter) acquire() {
+ l.mu.Lock()
+ for l.inUse >= l.max {
+ l.cond.Wait()
+ }
+ l.inUse++
+ l.mu.Unlock()
+}
+
+func (l *concLimiter) release() {
+ l.mu.Lock()
+ l.inUse--
+ l.cond.Signal()
+ l.mu.Unlock()
+}
+
+func (l *concLimiter) setMax(n int) {
+ if n < 1 {
+ n = 1
+ }
+ l.mu.Lock()
+ l.max = n
+ l.mu.Unlock()
+ l.cond.Broadcast() // newly-allowed slots: wake all waiters to re-check
+}
+
+// instLim caps concurrent in-use engine instances (MaxConcurrentInstances),
+// created once at initRuntime and resizable live via SetMaxConcurrent.
+var instLim *concLimiter
+
+// SetMaxConcurrent sets the chunker's instance-concurrency ceiling. Safe to call
+// before init (records the value for initRuntime) or live afterwards (resizes the
+// limiter without a restart — this is what the dashboard's chunk_max_concurrent
+// override calls).
+func SetMaxConcurrent(n int) {
+ if n < 1 {
+ n = 1
+ }
+ MaxConcurrentInstances = n
+ if instLim != nil {
+ instLim.setMax(n)
+ }
+}
+
+// baselineBytes is the linear-memory size of a fresh instance, captured once.
+var baselineBytes atomic.Uint64
+
+// Node is a decoded tree-sitter node from the batched dump.
+type Node struct {
+ KindID uint32
+ Kind string // resolved from KindID via the per-language symbol table
+ StartByte, EndByte uint32
+ StartRow, StartCol uint32
+ EndRow, EndCol uint32
+ Depth uint32
+ Named, Error, Missing, Extra bool
+}
+
+// ---------------------------------------------------------------------------
+// Shared runtime (compiled once) + engine pool
+// ---------------------------------------------------------------------------
+
+type runtime struct {
+ ctx context.Context
+ wz wazero.Runtime
+ cm wazero.CompiledModule
+}
+
+var (
+ rtOnce sync.Once
+ rt *runtime
+ rtErr error
+ gpool enginePool
+ instN atomic.Int64
+)
+
+func initRuntime() {
+ // On unix the engines' linear memory is mmap-backed (see mmap_unix.go):
+ // grow without realloc-copy, and munmap-on-close returns a recycled
+ // instance's memory to the OS immediately instead of parking it in the Go
+ // heap. The allocator rides the context, which wazero reads at
+ // InstantiateModule time.
+ ctx := context.Background()
+ if a := linearMemoryAllocator(); a != nil {
+ ctx = experimental.WithMemoryAllocator(ctx, a)
+ }
+ n := MaxConcurrentInstances
+ if n <= 0 {
+ n = 3
+ }
+ instLim = newConcLimiter(n)
+ cfg := wazero.NewRuntimeConfigCompiler()
+ if MemLimitPages > 0 {
+ cfg = cfg.WithMemoryLimitPages(MemLimitPages)
+ }
+ wz := wazero.NewRuntimeWithConfig(ctx, cfg)
+ if _, err := wasi_snapshot_preview1.Instantiate(ctx, wz); err != nil {
+ rtErr = fmt.Errorf("wasi: %w", err)
+ return
+ }
+ raw, err := io.ReadAll(brotli.NewReader(bytes.NewReader(wasmBrotli)))
+ if err != nil {
+ rtErr = fmt.Errorf("decompress ts-core.wasm.br: %w", err)
+ return
+ }
+ cm, err := wz.CompileModule(ctx, raw)
+ if err != nil {
+ rtErr = fmt.Errorf("compile ts-core.wasm: %w", err)
+ return
+ }
+ rt = &runtime{ctx: ctx, wz: wz, cm: cm}
+}
+
+// engine is one instantiated wasm module (own linear memory).
+type engine struct {
+ ctx context.Context
+ mod api.Module
+ mem api.Memory
+
+ malloc, free api.Function
+ parserNew, parserDelete, parserReset api.Function
+ setLang, parse, treeDelete api.Function
+ dumpTree api.Function
+ langSymCount, langSymName api.Function
+
+ langPtr map[string]uint32 // per-instance: TSLanguage* lives in this instance's memory
+}
+
+// symNameCache maps langExport → (symbol id → kind name). The id→name mapping is
+// a property of the compiled grammar, identical across ALL instances, so it is
+// cached GLOBALLY and built once per language — not once per instance. (Building
+// it costs ~1 s: one wazero call per grammar symbol, ~2000 for TypeScript. The
+// old per-instance cache meant every recycled/fresh instance paid that ~1 s on
+// its first file of a language — the "indexing hangs on this file" symptom.)
+var (
+ symNameMu sync.Mutex
+ symNameCache = map[string]map[uint32]string{}
+)
+
+func newEngine() (*engine, error) {
+ rtOnce.Do(initRuntime)
+ if rtErr != nil {
+ return nil, rtErr
+ }
+ name := fmt.Sprintf("ts-%d", instN.Add(1))
+ mod, err := rt.wz.InstantiateModule(rt.ctx, rt.cm,
+ wazero.NewModuleConfig().WithName(name).WithStartFunctions("_initialize"))
+ if err != nil {
+ return nil, fmt.Errorf("instantiate: %w", err)
+ }
+ e := &engine{
+ ctx: rt.ctx, mod: mod, mem: mod.Memory(),
+ malloc: mod.ExportedFunction("malloc"),
+ free: mod.ExportedFunction("free"),
+ parserNew: mod.ExportedFunction("ts_parser_new"),
+ parserDelete: mod.ExportedFunction("ts_parser_delete"),
+ parserReset: mod.ExportedFunction("ts_parser_reset"),
+ setLang: mod.ExportedFunction("ts_parser_set_language"),
+ parse: mod.ExportedFunction("ts_parser_parse_string"),
+ treeDelete: mod.ExportedFunction("ts_tree_delete"),
+ dumpTree: mod.ExportedFunction("ts_dump_tree"),
+ langSymCount: mod.ExportedFunction("ts_language_symbol_count"),
+ langSymName: mod.ExportedFunction("ts_language_symbol_name"),
+ langPtr: map[string]uint32{},
+ }
+ if rs := e.call(mod.ExportedFunction("ts_dump_rec_size")); rs != recSize {
+ mod.Close(rt.ctx)
+ return nil, fmt.Errorf("NodeRec size mismatch: guest=%d host=%d", rs, recSize)
+ }
+ baselineBytes.CompareAndSwap(0, e.memSize())
+ return e, nil
+}
+
+func (e *engine) close() { e.mod.Close(e.ctx) }
+
+// memSize is the engine's current linear-memory size in bytes.
+func (e *engine) memSize() uint64 { return uint64(e.mem.Size()) }
+
+// enginePool is a bounded pool with high-water-mark recycling. Because wasm
+// linear memory only grows, an instance that ballooned parsing one huge file is
+// CLOSED on release (RecycleBytes) instead of kept holding that memory; idle
+// instances are capped (MaxIdleInstances). The atomic counters feed PoolStats()
+// so the operator can watch memory under concurrent sync.
+type enginePool struct {
+ mu sync.Mutex
+ free []*engine
+
+ created atomic.Int64
+ closed atomic.Int64
+ recycled atomic.Int64
+}
+
+func (p *enginePool) maxIdle() int {
+ if MaxIdleInstances > 0 {
+ return MaxIdleInstances
+ }
+ if MaxConcurrentInstances > 0 {
+ return MaxConcurrentInstances
+ }
+ return 3
+}
+
+// acquire takes a concurrency slot (blocking until one frees — this is the hard
+// cap that decouples chunker parallelism from embedding concurrency) then returns
+// a pooled-or-fresh engine. The slot is released by release().
+func (p *enginePool) acquire() (*engine, error) {
+ // Ensure the runtime (and instLim) exist before taking a slot — newEngine
+ // also triggers this, but instLim.acquire() runs first.
+ rtOnce.Do(initRuntime)
+ if rtErr != nil {
+ return nil, rtErr
+ }
+ instLim.acquire()
+ p.mu.Lock()
+ if n := len(p.free); n > 0 {
+ e := p.free[n-1]
+ p.free[n-1] = nil
+ p.free = p.free[:n-1]
+ p.mu.Unlock()
+ return e, nil
+ }
+ p.mu.Unlock()
+ e, err := newEngine()
+ if err != nil {
+ instLim.release() // failed to create → give the slot back
+ return nil, err
+ }
+ p.created.Add(1)
+ return e, nil
+}
+
+// release returns an engine to the pool, or closes it when: tainted (a trap may
+// have left guest allocations un-freed), it grew past RecycleGrowthBytes, or the
+// idle pool is full. It always frees the concurrency slot taken by acquire().
+func (p *enginePool) release(e *engine, tainted bool) {
+ defer instLim.release()
+ switch {
+ case tainted:
+ p.discard(e)
+ case RecycleGrowthBytes > 0 && e.memSize() > baselineBytes.Load()+RecycleGrowthBytes:
+ p.recycled.Add(1)
+ p.discard(e)
+ default:
+ p.mu.Lock()
+ if len(p.free) >= p.maxIdle() {
+ p.mu.Unlock()
+ p.discard(e)
+ return
+ }
+ p.free = append(p.free, e)
+ p.mu.Unlock()
+ }
+}
+
+func (p *enginePool) discard(e *engine) {
+ e.close()
+ p.closed.Add(1)
+}
+
+// Stats is a snapshot of the engine pool for memory observability.
+type Stats struct {
+ Created int64 // instances ever created
+ Closed int64 // instances closed (recycled + over-idle + tainted)
+ Recycled int64 // subset of Closed: closed due to the RecycleBytes high-water-mark
+ LiveOpen int64 // Created-Closed (idle + checked-out)
+ IdlePooled int // instances currently idle in the pool
+ IdleMemBytes uint64 // sum of idle instances' current linear memory
+}
+
+// PoolStats returns a live snapshot for /health or a debug endpoint.
+func PoolStats() Stats {
+ gpool.mu.Lock()
+ idle := len(gpool.free)
+ var mem uint64
+ for _, e := range gpool.free {
+ mem += e.memSize()
+ }
+ gpool.mu.Unlock()
+ created, closed := gpool.created.Load(), gpool.closed.Load()
+ return Stats{
+ Created: created,
+ Closed: closed,
+ Recycled: gpool.recycled.Load(),
+ LiveOpen: created - closed,
+ IdlePooled: idle,
+ IdleMemBytes: mem,
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+// HasLanguage reports whether the module exports the given grammar (e.g.
+// "tree_sitter_go"). Cheap; instantiates the pool lazily.
+func HasLanguage(langExport string) bool {
+ e, err := gpool.acquire()
+ if err != nil {
+ return false
+ }
+ defer gpool.release(e, false)
+ return e.mod.ExportedFunction(langExport) != nil
+}
+
+// ParseNodes parses src under the grammar export and returns the whole tree as a
+// flat pre-order slice (batched via ts_dump_tree). A guest trap is returned as an
+// error and the tainted instance is discarded; the host process stays alive.
+func ParseNodes(langExport string, src []byte) ([]Node, error) {
+ e, err := gpool.acquire()
+ if err != nil {
+ return nil, err
+ }
+ nodes, perr := e.parseNodes(langExport, src)
+ // On error the instance may be tainted (a trap can leave guest allocations
+ // un-freed) → release() closes it rather than pooling it.
+ gpool.release(e, perr != nil)
+ if perr != nil {
+ return nil, perr
+ }
+ return nodes, nil
+}
+
+func (e *engine) call(f api.Function, args ...uint64) uint64 {
+ r, err := f.Call(e.ctx, args...)
+ if err != nil {
+ panic(err)
+ }
+ if len(r) == 0 {
+ return 0
+ }
+ return r[0]
+}
+
+func (e *engine) language(export string) (uint32, bool) {
+ if p, ok := e.langPtr[export]; ok {
+ return p, p != 0
+ }
+ fn := e.mod.ExportedFunction(export)
+ if fn == nil {
+ e.langPtr[export] = 0
+ return 0, false
+ }
+ p := uint32(e.call(fn))
+ e.langPtr[export] = p
+ return p, p != 0
+}
+
+func (e *engine) symbolNames(export string, lang uint32) map[uint32]string {
+ symNameMu.Lock()
+ m, ok := symNameCache[export]
+ symNameMu.Unlock()
+ if ok {
+ return m
+ }
+ // Build outside the lock (the ~2000 wazero calls are slow). The result is
+ // module-global, so a rare concurrent double-build is harmless — both produce
+ // the identical map and last-writer-wins.
+ count := uint32(e.call(e.langSymCount, uint64(lang)))
+ m = make(map[uint32]string, count)
+ for id := range count {
+ ptr := uint32(e.call(e.langSymName, uint64(lang), uint64(id)))
+ m[id] = e.readCStr(ptr)
+ }
+ symNameMu.Lock()
+ symNameCache[export] = m
+ symNameMu.Unlock()
+ return m
+}
+
+func (e *engine) parseNodes(langExport string, src []byte) (nodes []Node, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("wasm trap (contained): %v", r)
+ }
+ }()
+
+ lang, ok := e.language(langExport)
+ if !ok {
+ return nil, fmt.Errorf("unknown grammar export %q", langExport)
+ }
+ parser := e.call(e.parserNew)
+ defer e.call(e.parserDelete, parser)
+ e.call(e.setLang, parser, uint64(lang))
+
+ sp := uint32(e.call(e.malloc, uint64(len(src)+1)))
+ e.mem.Write(sp, src)
+ e.mem.WriteByte(sp+uint32(len(src)), 0)
+ defer e.call(e.free, uint64(sp))
+
+ tree := e.call(e.parse, parser, 0, uint64(sp), uint64(len(src)))
+ if tree == 0 {
+ return nil, fmt.Errorf("parse returned null tree")
+ }
+ defer e.call(e.treeDelete, tree)
+
+ n := uint32(e.call(e.dumpTree, tree, 0, 0))
+ if n == 0 {
+ return nil, nil
+ }
+ buf := uint32(e.call(e.malloc, uint64(n)*recSize))
+ defer e.call(e.free, uint64(buf))
+ got := uint32(e.call(e.dumpTree, tree, uint64(buf), uint64(n)))
+ if got != n {
+ return nil, fmt.Errorf("dump count changed between passes: %d vs %d", n, got)
+ }
+
+ raw, ok2 := e.mem.Read(buf, n*recSize)
+ if !ok2 {
+ return nil, fmt.Errorf("read dump buffer failed (ptr=%d len=%d)", buf, n*recSize)
+ }
+ names := e.symbolNames(langExport, lang)
+
+ nodes = make([]Node, n)
+ for i := range n {
+ o := i * recSize
+ kindID := binary.LittleEndian.Uint32(raw[o:])
+ flags := binary.LittleEndian.Uint32(raw[o+32:])
+ nodes[i] = Node{
+ KindID: kindID,
+ Kind: names[kindID],
+ StartByte: binary.LittleEndian.Uint32(raw[o+4:]),
+ EndByte: binary.LittleEndian.Uint32(raw[o+8:]),
+ StartRow: binary.LittleEndian.Uint32(raw[o+12:]),
+ StartCol: binary.LittleEndian.Uint32(raw[o+16:]),
+ EndRow: binary.LittleEndian.Uint32(raw[o+20:]),
+ EndCol: binary.LittleEndian.Uint32(raw[o+24:]),
+ Depth: binary.LittleEndian.Uint32(raw[o+28:]),
+ Named: flags&1 != 0,
+ Error: flags&2 != 0,
+ Missing: flags&4 != 0,
+ Extra: flags&8 != 0,
+ }
+ }
+ return nodes, nil
+}
+
+func (e *engine) readCStr(ptr uint32) string {
+ if ptr == 0 {
+ return ""
+ }
+ var b []byte
+ for off := ptr; ; off++ {
+ c, ok := e.mem.ReadByte(off)
+ if !ok || c == 0 {
+ break
+ }
+ b = append(b, c)
+ }
+ return string(b)
+}
diff --git a/server/internal/chunker/tswasm/tswasm_test.go b/server/internal/chunker/tswasm/tswasm_test.go
new file mode 100644
index 0000000..95d096a
--- /dev/null
+++ b/server/internal/chunker/tswasm/tswasm_test.go
@@ -0,0 +1,139 @@
+package tswasm
+
+import (
+ "strings"
+ "sync"
+ "testing"
+)
+
+func TestParseNodes_Go(t *testing.T) {
+ nodes, err := ParseNodes("tree_sitter_go", []byte("package m\nfunc Hello() int { return 1 }\ntype T struct{ X int }\n"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := map[string]bool{"function_declaration": false, "type_declaration": false}
+ for _, n := range nodes {
+ if _, ok := want[n.Kind]; ok {
+ want[n.Kind] = true
+ }
+ if n.Error || n.Missing {
+ t.Errorf("unexpected error/missing node %q", n.Kind)
+ }
+ }
+ for k, found := range want {
+ if !found {
+ t.Errorf("kind %q not found", k)
+ }
+ }
+}
+
+func TestHasLanguage(t *testing.T) {
+ if !HasLanguage("tree_sitter_python") {
+ t.Error("python should be present")
+ }
+ if HasLanguage("tree_sitter_nonexistent") {
+ t.Error("nonexistent grammar should be absent")
+ }
+}
+
+// TestPool_RecycleAndStats verifies the high-water-mark recycling and the
+// observability snapshot. With RecycleBytes forced below an instance's static
+// memory floor, every release recycles (closes) the instance instead of pooling
+// it — so memory never accumulates and the counters move.
+func TestPool_RecycleAndStats(t *testing.T) {
+ old := RecycleGrowthBytes
+ RecycleGrowthBytes = 1 << 20 // recycle if an instance grew >1 MiB over baseline
+ defer func() { RecycleGrowthBytes = old }()
+
+ // A big parse balloons linear memory far past baseline → must recycle.
+ // ~500 KiB ≈ the indexer's maxContentBytes cap; grows an instance by
+ // ~40-60 MiB, comfortably under MemLimitPages but far over the forced
+ // 1 MiB recycle threshold above.
+ big := []byte(strings.Repeat("func F(){ x:=1; _=x }\n", 24000)) // ~520 KiB
+ before := PoolStats()
+ for range 3 {
+ if _, err := ParseNodes("tree_sitter_go", big); err != nil {
+ t.Fatal(err)
+ }
+ }
+ after := PoolStats()
+
+ if after.Recycled-before.Recycled < 3 {
+ t.Errorf("expected ≥3 recycles, got %d", after.Recycled-before.Recycled)
+ }
+ t.Logf("stats: created=%d closed=%d recycled=%d liveOpen=%d idle=%d",
+ after.Created, after.Closed, after.Recycled, after.LiveOpen, after.IdlePooled)
+}
+
+// TestPool_ReuseWhenSmall verifies that normal (small) parses pool-and-reuse the
+// instance rather than recycling — created count must NOT grow once warm.
+func TestPool_ReuseWhenSmall(t *testing.T) {
+ // default RecycleBytes (64 MiB) — a tiny parse stays well under it.
+ _, _ = ParseNodes("tree_sitter_go", []byte("package m\n")) // warm the pool
+ mid := PoolStats()
+ for range 10 {
+ if _, err := ParseNodes("tree_sitter_go", []byte("package m\nfunc G(){}\n")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ after := PoolStats()
+ if after.Created != mid.Created {
+ t.Errorf("warm pool should reuse instances, but Created grew %d→%d", mid.Created, after.Created)
+ }
+}
+
+// TestParseNodes_Concurrent exercises the engine pool under concurrency — each
+// goroutine must get its own instance (wazero modules are not concurrency-safe).
+func TestParseNodes_Concurrent(t *testing.T) {
+ var wg sync.WaitGroup
+ src := []byte(strings.Repeat("func F(){}\n", 50))
+ for range 16 {
+ wg.Go(func() {
+ for range 10 {
+ if _, err := ParseNodes("tree_sitter_go", src); err != nil {
+ t.Errorf("parse: %v", err)
+ return
+ }
+ }
+ })
+ }
+ wg.Wait()
+}
+
+func TestInstanceSemaphore(t *testing.T) {
+ if _, err := ParseNodes("tree_sitter_go", []byte("package m\n")); err != nil {
+ t.Fatal(err) // triggers init
+ }
+ if instLim == nil {
+ t.Fatal("limiter not initialized")
+ }
+ instLim.mu.Lock()
+ max, inUse := instLim.max, instLim.inUse
+ instLim.mu.Unlock()
+ if max != MaxConcurrentInstances {
+ t.Errorf("limiter max = %d, want MaxConcurrentInstances = %d", max, MaxConcurrentInstances)
+ }
+ if inUse != 0 {
+ t.Errorf("limiter should be drained after release, got %d in use", inUse)
+ }
+}
+
+func TestSetMaxConcurrent_Live(t *testing.T) {
+ if _, err := ParseNodes("tree_sitter_go", []byte("package m\n")); err != nil {
+ t.Fatal(err) // ensure init
+ }
+ orig := MaxConcurrentInstances
+ defer SetMaxConcurrent(orig)
+
+ SetMaxConcurrent(7)
+ instLim.mu.Lock()
+ max := instLim.max
+ instLim.mu.Unlock()
+ if max != 7 || MaxConcurrentInstances != 7 {
+ t.Errorf("live resize failed: limiter.max=%d MaxConcurrentInstances=%d, want 7", max, MaxConcurrentInstances)
+ }
+ // still parses correctly after a live resize
+ if _, err := ParseNodes("tree_sitter_go", []byte("package m\nfunc F(){}\n")); err != nil {
+ t.Fatalf("parse after resize: %v", err)
+ }
+}
diff --git a/server/internal/chunksfts/chunksfts.go b/server/internal/chunksfts/chunksfts.go
index 4077e55..4986a5d 100644
--- a/server/internal/chunksfts/chunksfts.go
+++ b/server/internal/chunksfts/chunksfts.go
@@ -138,38 +138,60 @@ func DeleteByFileTx(ctx context.Context, tx *sql.Tx, projectPath, filePath strin
return nil
}
-// DeleteByProjectTx wipes every BM25 row for a project. Used by the
-// indexer's full-reindex wipe path and by projects.Delete so removing a
-// project leaves no stranded FTS rows.
-func DeleteByProjectTx(ctx context.Context, tx *sql.Tx, projectPath string) error {
- if _, err := tx.ExecContext(ctx,
- `DELETE FROM chunks_fts
- WHERE rowid IN (SELECT rowid FROM chunks_meta WHERE project_path = ?)`,
- projectPath,
- ); err != nil {
- return fmt.Errorf("delete chunks_fts by project: %w", err)
- }
- if _, err := tx.ExecContext(ctx,
- `DELETE FROM chunks_meta WHERE project_path = ?`,
- projectPath,
- ); err != nil {
- return fmt.Errorf("delete chunks_meta by project: %w", err)
- }
- return nil
-}
+// deleteBatchSize bounds one wipe batch. FTS5 deletes re-tokenize each row's
+// content to remove its postings — with the trigram tokenizer and ~4.5 KB
+// chunks that is by far the most expensive delete in the schema, so the batch
+// is sized to keep each transaction well under a second. The point of
+// batching: SQLite has ONE writer, and a monolithic project wipe (vscode-sized
+// projects: tens of thousands of FTS rows) held the write lock for minutes,
+// starving every other writer past busy_timeout (the prod symptom: jobs-worker
+// `claim failed: SQLITE_BUSY` every poll tick for the whole wipe).
+const deleteBatchSize = 500
-// DeleteByProject is the non-tx form for callers that don't already hold
-// one (admin DeleteProject handler, manual cleanup).
+// DeleteByProject wipes a project's BM25 rows in bounded batches, one
+// transaction per batch, releasing the SQLite writer between batches so
+// concurrent writers (jobs queue, dashboard, other indexing runs) interleave.
+//
+// NOT atomic as a whole: a crash mid-wipe leaves the tail in place. Callers
+// (full-reindex wipe, project delete) tolerate that — the next wipe attempt
+// resumes where this one stopped, and orphaned rows are invisible to search
+// once the project row / file_hashes are gone.
+//
+// Both subselects inside a batch tx see the same snapshot and use
+// ORDER BY rowid, so the fts and meta deletes target the identical row set.
func DeleteByProject(ctx context.Context, db *sql.DB, projectPath string) error {
- tx, err := db.BeginTx(ctx, nil)
- if err != nil {
- return err
- }
- defer tx.Rollback() //nolint:errcheck // no-op after commit
- if err := DeleteByProjectTx(ctx, tx, projectPath); err != nil {
- return err
+ for {
+ tx, err := db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ if _, err := tx.ExecContext(ctx,
+ `DELETE FROM chunks_fts
+ WHERE rowid IN (SELECT rowid FROM chunks_meta
+ WHERE project_path = ? ORDER BY rowid LIMIT ?)`,
+ projectPath, deleteBatchSize,
+ ); err != nil {
+ tx.Rollback() //nolint:errcheck
+ return fmt.Errorf("delete chunks_fts batch: %w", err)
+ }
+ res, err := tx.ExecContext(ctx,
+ `DELETE FROM chunks_meta
+ WHERE rowid IN (SELECT rowid FROM chunks_meta
+ WHERE project_path = ? ORDER BY rowid LIMIT ?)`,
+ projectPath, deleteBatchSize,
+ )
+ if err != nil {
+ tx.Rollback() //nolint:errcheck
+ return fmt.Errorf("delete chunks_meta batch: %w", err)
+ }
+ n, _ := res.RowsAffected()
+ if err := tx.Commit(); err != nil {
+ return err
+ }
+ if n < deleteBatchSize {
+ return nil
+ }
}
- return tx.Commit()
}
// SearchProject runs an OR-joined trigram FTS5 query restricted to a
diff --git a/server/internal/config/config.go b/server/internal/config/config.go
index cf98a63..b496b95 100644
--- a/server/internal/config/config.go
+++ b/server/internal/config/config.go
@@ -41,6 +41,13 @@ type Config struct {
// Dashboard-overridable via runtimecfg. Env: CIX_INDEX_EMBED_BATCH_CHUNKS.
IndexEmbedBatchChunks int
+ // ChunkMaxConcurrent caps how many tree-sitter (wasm) parser instances run
+ // at once — the chunker's OWN concurrency, decoupled from embedding
+ // concurrency. Each instance holds ~69 MiB, so this bounds peak chunker
+ // memory regardless of how many files embed in parallel. 0 → recommended (3).
+ // Dashboard-overridable via runtimecfg. Env: CIX_CHUNK_MAX_CONCURRENT.
+ ChunkMaxConcurrent int
+
// Phase 3 — llama-server sidecar configuration.
GGUFPath string // CIX_GGUF_PATH; absolute path. Empty = auto-resolve via cache / dev-fallback / HF download.
GGUFCacheDir string // CIX_GGUF_CACHE_DIR; where HF downloads land.
@@ -51,6 +58,7 @@ type Config struct {
LlamaNGpuLayers int // CIX_N_GPU_LAYERS; -1 on darwin (Metal all layers), 0 elsewhere.
LlamaNThreads int // CIX_LLAMA_THREADS; CPU thread count for llama-server (--threads). 0 = auto.
LlamaBatchSize int // CIX_LLAMA_BATCH; llama-server logical batch size (-b). 0 = match LlamaCtxSize.
+ LlamaCacheRAMMiB int // CIX_LLAMA_CACHE_RAM; llama-server host prompt-cache cap in MiB (--cache-ram). 0 = disabled (embeddings get zero prompt reuse; upstream default 8192 caused OOM kills), -1 = unlimited.
LlamaStartupSec int // CIX_LLAMA_STARTUP_TIMEOUT; readiness probe ceiling in seconds.
EmbeddingsEnabled bool // CIX_EMBEDDINGS_ENABLED; test hook to bypass sidecar entirely.
@@ -271,6 +279,12 @@ func Load() (*Config, error) {
}
c.IndexEmbedBatchChunks = idxBatch
+ chunkConc, err := getenvInt("CIX_CHUNK_MAX_CONCURRENT", 0)
+ if err != nil {
+ return nil, err
+ }
+ c.ChunkMaxConcurrent = chunkConc
+
maxChunk, err := getenvInt("CIX_MAX_CHUNK_TOKENS", 1500)
if err != nil {
return nil, err
@@ -330,6 +344,16 @@ func Load() (*Config, error) {
}
c.LlamaBatchSize = batch
+ // CIX_LLAMA_CACHE_RAM: llama-server's host prompt cache, in MiB. The
+ // upstream default (8192) is pure host-RAM waste for an embeddings-only
+ // sidecar — prompts are never reused — and grew llama-server's RSS until
+ // the container OOM-killed it. 0 (our default) disables it; -1 = unlimited.
+ cacheRAM, err := getenvInt("CIX_LLAMA_CACHE_RAM", 0)
+ if err != nil {
+ return nil, err
+ }
+ c.LlamaCacheRAMMiB = cacheRAM
+
startup, err := getenvInt("CIX_LLAMA_STARTUP_TIMEOUT", 60)
if err != nil {
return nil, err
diff --git a/server/internal/db/db.go b/server/internal/db/db.go
index d0fccf1..889daeb 100644
--- a/server/internal/db/db.go
+++ b/server/internal/db/db.go
@@ -68,6 +68,8 @@ var registeredMigrations = []migration{
{13, "indexed_with_model_provider_prefix", func(db *sql.DB, _ OpenOptions) error { return migrateIndexedWithModelProviderPrefix(db) }},
{14, "user_local_project_disabled", func(db *sql.DB, _ OpenOptions) error { return migrateUserLocalProjectDisabled(db) }},
{15, "index_embed_batch_chunks", func(db *sql.DB, _ OpenOptions) error { return migrateIndexEmbedBatchChunks(db) }},
+ {16, "chunk_max_concurrent", func(db *sql.DB, _ OpenOptions) error { return migrateChunkMaxConcurrent(db) }},
+ {17, "llama_cache_ram_mib", func(db *sql.DB, _ OpenOptions) error { return migrateAddRuntimeSettingsColumn(db, "llama_cache_ram_mib") }},
}
// DriverName is the registered database/sql driver name for modernc.org/sqlite.
@@ -840,6 +842,69 @@ func migrateIndexEmbedBatchChunks(db *sql.DB) error {
return nil
}
+// migrateAddRuntimeSettingsColumn adds an INTEGER column to runtime_settings.
+// Idempotent: skips the ALTER when the column already exists. Used by
+// migration 17 (llama_cache_ram_mib — the llama-server host prompt-cache cap,
+// dashboard-overridable) and any future single-column runtime_settings adds.
+func migrateAddRuntimeSettingsColumn(db *sql.DB, column string) error {
+ rows, err := db.Query(`PRAGMA table_info(runtime_settings)`)
+ if err != nil {
+ return fmt.Errorf("table_info runtime_settings: %w", err)
+ }
+ have := map[string]bool{}
+ for rows.Next() {
+ var (
+ cid int
+ name, typ string
+ notnull, pk int
+ dflt sql.NullString
+ )
+ if err := rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk); err != nil {
+ rows.Close()
+ return err
+ }
+ have[name] = true
+ }
+ rows.Close()
+ if !have[column] {
+ if _, err := db.Exec(`ALTER TABLE runtime_settings ADD COLUMN ` + column + ` INTEGER`); err != nil {
+ return fmt.Errorf("add %s column: %w", column, err)
+ }
+ }
+ return nil
+}
+
+// migrateChunkMaxConcurrent adds runtime_settings.chunk_max_concurrent (the
+// tree-sitter wasm chunker's instance-concurrency cap, dashboard-overridable).
+// Idempotent: skips the ALTER when the column already exists.
+func migrateChunkMaxConcurrent(db *sql.DB) error {
+ rows, err := db.Query(`PRAGMA table_info(runtime_settings)`)
+ if err != nil {
+ return fmt.Errorf("table_info runtime_settings: %w", err)
+ }
+ have := map[string]bool{}
+ for rows.Next() {
+ var (
+ cid int
+ name, typ string
+ notnull, pk int
+ dflt sql.NullString
+ )
+ if err := rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk); err != nil {
+ rows.Close()
+ return err
+ }
+ have[name] = true
+ }
+ rows.Close()
+ if !have["chunk_max_concurrent"] {
+ if _, err := db.Exec(`ALTER TABLE runtime_settings ADD COLUMN chunk_max_concurrent INTEGER`); err != nil {
+ return fmt.Errorf("add chunk_max_concurrent column: %w", err)
+ }
+ }
+ return nil
+}
+
// migrateIndexedWithModelProviderPrefix backfills projects indexed
// before the pluggable-provider refactor (migration 12). Pre-refactor
// the indexer wrote a bare model name like
diff --git a/server/internal/db/schema.go b/server/internal/db/schema.go
index 5e76df6..631c226 100644
--- a/server/internal/db/schema.go
+++ b/server/internal/db/schema.go
@@ -184,7 +184,15 @@ CREATE TABLE IF NOT EXISTS runtime_settings (
embedding_provider_config TEXT,
-- Cross-file embed-batch size for repo indexing (added in migration 15).
-- NULL → fall through to env / recommended.
- index_embed_batch_chunks INTEGER
+ index_embed_batch_chunks INTEGER,
+ -- Chunker (tree-sitter wasm) instance-concurrency cap (added in migration 16).
+ -- NULL → fall through to env / recommended (3).
+ chunk_max_concurrent INTEGER,
+ -- llama-server host prompt-cache cap in MiB, --cache-ram (added in
+ -- migration 17). NULL → fall through to env / recommended (0 = disabled:
+ -- embeddings never reuse prompts, and llama's own 8 GiB default
+ -- OOM-killed the prod container). -1 = unlimited.
+ llama_cache_ram_mib INTEGER
);
-- Workspaces group indexed projects (rows in the projects table,
diff --git a/server/internal/embeddings/provider/ollama/provider.go b/server/internal/embeddings/provider/ollama/provider.go
index 9c7173a..c3f40a4 100644
--- a/server/internal/embeddings/provider/ollama/provider.go
+++ b/server/internal/embeddings/provider/ollama/provider.go
@@ -66,6 +66,12 @@ type Config struct {
// BatchSize: 0 = match CtxSize.
BatchSize int `json:"batch_size,omitempty"`
+ // CacheRAMMiB caps llama-server's host prompt cache (--cache-ram).
+ // 0 = disabled (the right call for embeddings: prompts are never
+ // reused, and llama's own 8 GiB default OOM-killed the prod
+ // container); -1 = unlimited; N > 0 = cap in MiB.
+ CacheRAMMiB int `json:"cache_ram_mib,omitempty"`
+
// StartupSec bounds the readiness probe.
StartupSec int `json:"startup_sec,omitempty"`
}
@@ -151,6 +157,7 @@ func (p *Provider) Start(ctx context.Context) error {
NGpuLayers: p.cfg.NGpuLayers,
NThreads: p.cfg.NThreads,
BatchSize: p.cfg.BatchSize,
+ CacheRAM: p.cfg.CacheRAMMiB,
StartupSec: p.cfg.StartupSec,
Model: p.cfg.Model,
}
diff --git a/server/internal/embeddings/provider/ollama/supervisor.go b/server/internal/embeddings/provider/ollama/supervisor.go
index bd17111..b9e9e86 100644
--- a/server/internal/embeddings/provider/ollama/supervisor.go
+++ b/server/internal/embeddings/provider/ollama/supervisor.go
@@ -44,6 +44,7 @@ type supervisorConfig struct {
NGpuLayers int
NThreads int // 0 = let llama-server auto-detect via hardware_concurrency
BatchSize int // 0 = match CtxSize (preserves prior --ubatch-size behaviour)
+ CacheRAM int // --cache-ram MiB; 0 = disabled, -1 = unlimited
StartupSec int
TCPPort int // 0 = auto-pick, only relevant for tcp transport
// Model is the human-readable identifier (HF repo id or absolute path)
@@ -190,6 +191,14 @@ func (s *supervisor) spawn(ctx context.Context) error {
// any chunk larger than 512 tokens.
"--ubatch-size", strconv.Itoa(s.cfg.CtxSize),
"--n-gpu-layers", strconv.Itoa(s.cfg.NGpuLayers),
+ // llama-server ships a HOST-RAM prompt cache that defaults to 8 GiB
+ // (--cache-ram, since ggml-org/llama.cpp#16391). For an embedding
+ // server it is pure waste — every chunk is a unique prompt, reuse is
+ // zero — but the cache still fills, growing llama-server's RSS
+ // monotonically (observed: 365 MB → 11.3 GB during one vscode index)
+ // until the container's cgroup OOM-kills it. Default 0 disables it;
+ // dashboard-overridable via runtime-config llama_cache_ram_mib.
+ "--cache-ram", strconv.Itoa(s.cfg.CacheRAM),
}
// PR-E — only pass --threads when the operator explicitly set one. With
// 0 we let llama-server pick via std::thread::hardware_concurrency, which
diff --git a/server/internal/embeddings/service.go b/server/internal/embeddings/service.go
index 581b0dc..13d7060 100644
--- a/server/internal/embeddings/service.go
+++ b/server/internal/embeddings/service.go
@@ -183,6 +183,7 @@ func BuildOllamaConfigFromEnv(cfg *config.Config) ([]byte, error) {
NGpuLayers: cfg.LlamaNGpuLayers,
NThreads: cfg.LlamaNThreads,
BatchSize: cfg.LlamaBatchSize,
+ CacheRAMMiB: cfg.LlamaCacheRAMMiB,
StartupSec: cfg.LlamaStartupSec,
}
return json.Marshal(c)
@@ -330,6 +331,7 @@ func buildOllamaFromConfig(cfg *config.Config, logger *slog.Logger) (provider.Pr
NGpuLayers: cfg.LlamaNGpuLayers,
NThreads: cfg.LlamaNThreads,
BatchSize: cfg.LlamaBatchSize,
+ CacheRAMMiB: cfg.LlamaCacheRAMMiB,
StartupSec: cfg.LlamaStartupSec,
}
b, err := json.Marshal(c)
diff --git a/server/internal/httpapi/admin_server.go b/server/internal/httpapi/admin_server.go
index 630bca7..4be34b5 100644
--- a/server/internal/httpapi/admin_server.go
+++ b/server/internal/httpapi/admin_server.go
@@ -18,6 +18,7 @@ import (
"sync/atomic"
"time"
+ "github.com/dvcdsys/code-index/server/internal/chunker/tswasm"
"github.com/dvcdsys/code-index/server/internal/embeddings"
"github.com/dvcdsys/code-index/server/internal/embeddings/provider"
"github.com/dvcdsys/code-index/server/internal/runtimecfg"
@@ -37,6 +38,8 @@ type runtimeConfigPayload struct {
MaxEmbeddingConcurrency int `json:"max_embedding_concurrency"`
LlamaBatchSize int `json:"llama_batch_size"`
IndexEmbedBatchChunks int `json:"index_embed_batch_chunks"`
+ ChunkMaxConcurrent int `json:"chunk_max_concurrent"`
+ LlamaCacheRAMMiB int `json:"llama_cache_ram_mib"`
Source map[string]string `json:"source"`
Recommended *recommendedSnapshotPayload `json:"recommended,omitempty"`
UpdatedAt *string `json:"updated_at,omitempty"`
@@ -51,6 +54,8 @@ type recommendedSnapshotPayload struct {
MaxEmbeddingConcurrency int `json:"max_embedding_concurrency"`
LlamaBatchSize int `json:"llama_batch_size"`
IndexEmbedBatchChunks int `json:"index_embed_batch_chunks"`
+ ChunkMaxConcurrent int `json:"chunk_max_concurrent"`
+ LlamaCacheRAMMiB int `json:"llama_cache_ram_mib"`
}
func snapshotToPayload(snap runtimecfg.Snapshot, rec runtimecfg.Snapshot) runtimeConfigPayload {
@@ -62,6 +67,8 @@ func snapshotToPayload(snap runtimecfg.Snapshot, rec runtimecfg.Snapshot) runtim
MaxEmbeddingConcurrency: snap.MaxEmbeddingConcurrency,
LlamaBatchSize: snap.LlamaBatchSize,
IndexEmbedBatchChunks: snap.IndexEmbedBatchChunks,
+ ChunkMaxConcurrent: snap.ChunkMaxConcurrent,
+ LlamaCacheRAMMiB: snap.LlamaCacheRAMMiB,
Source: snap.Source,
Recommended: &recommendedSnapshotPayload{
EmbeddingModel: rec.EmbeddingModel,
@@ -71,6 +78,8 @@ func snapshotToPayload(snap runtimecfg.Snapshot, rec runtimecfg.Snapshot) runtim
MaxEmbeddingConcurrency: rec.MaxEmbeddingConcurrency,
LlamaBatchSize: rec.LlamaBatchSize,
IndexEmbedBatchChunks: rec.IndexEmbedBatchChunks,
+ ChunkMaxConcurrent: rec.ChunkMaxConcurrent,
+ LlamaCacheRAMMiB: rec.LlamaCacheRAMMiB,
},
}
if !snap.UpdatedAt.IsZero() {
@@ -124,6 +133,8 @@ func (s *Server) PutRuntimeConfig(w http.ResponseWriter, r *http.Request) {
MaxEmbeddingConcurrency *int `json:"max_embedding_concurrency"`
LlamaBatchSize *int `json:"llama_batch_size"`
IndexEmbedBatchChunks *int `json:"index_embed_batch_chunks"`
+ ChunkMaxConcurrent *int `json:"chunk_max_concurrent"`
+ LlamaCacheRAMMiB *int `json:"llama_cache_ram_mib"`
}
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
@@ -156,6 +167,14 @@ func (s *Server) PutRuntimeConfig(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusUnprocessableEntity, "index_embed_batch_chunks must be >= 0")
return
}
+ if body.ChunkMaxConcurrent != nil && *body.ChunkMaxConcurrent < 0 {
+ writeError(w, http.StatusUnprocessableEntity, "chunk_max_concurrent must be >= 0")
+ return
+ }
+ if body.LlamaCacheRAMMiB != nil && *body.LlamaCacheRAMMiB < -1 {
+ writeError(w, http.StatusUnprocessableEntity, "llama_cache_ram_mib must be >= -1 (-1 = unlimited, 0 = disabled)")
+ return
+ }
patch := runtimecfg.Patch{
EmbeddingModel: body.EmbeddingModel,
@@ -165,6 +184,8 @@ func (s *Server) PutRuntimeConfig(w http.ResponseWriter, r *http.Request) {
MaxEmbeddingConcurrency: body.MaxEmbeddingConcurrency,
LlamaBatchSize: body.LlamaBatchSize,
IndexEmbedBatchChunks: body.IndexEmbedBatchChunks,
+ ChunkMaxConcurrent: body.ChunkMaxConcurrent,
+ LlamaCacheRAMMiB: body.LlamaCacheRAMMiB,
}
updatedBy := ""
if ac != nil {
@@ -179,6 +200,10 @@ func (s *Server) PutRuntimeConfig(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, "saved but could not reload runtime config")
return
}
+ // Apply the chunker concurrency live (resizes the wasm instance limiter
+ // without a restart). Embedding-side fields are read dynamically by the
+ // indexer, so they need no explicit apply here.
+ tswasm.SetMaxConcurrent(snap.ChunkMaxConcurrent)
writeJSON(w, http.StatusOK, snapshotToPayload(snap, s.Deps.RuntimeCfg.Recommended()))
}
diff --git a/server/internal/httpapi/openapi/openapi.gen.go b/server/internal/httpapi/openapi/openapi.gen.go
index a47c619..2288af7 100644
--- a/server/internal/httpapi/openapi/openapi.gen.go
+++ b/server/internal/httpapi/openapi/openapi.gen.go
@@ -1851,13 +1851,19 @@ type RestartAccepted struct {
// RuntimeConfig defines model for RuntimeConfig.
type RuntimeConfig struct {
+ // ChunkMaxConcurrent Chunker (tree-sitter wasm) instance-concurrency cap, decoupled from embedding concurrency. Each instance holds ~69 MiB. 0 = recommended (3).
+ ChunkMaxConcurrent int `json:"chunk_max_concurrent"`
+
// EmbeddingModel HF repo ID or absolute filesystem path to a .gguf file.
EmbeddingModel string `json:"embedding_model"`
// IndexEmbedBatchChunks Cross-file embed-batch size for repo indexing (chunks per embed call). 0 = one call per file.
IndexEmbedBatchChunks int `json:"index_embed_batch_chunks"`
LlamaBatchSize int `json:"llama_batch_size"`
- LlamaCtxSize int `json:"llama_ctx_size"`
+
+ // LlamaCacheRamMib llama-server host prompt-cache cap in MiB (--cache-ram). 0 = disabled (recommended for embeddings — prompts are never reused, and llama's upstream 8 GiB default grows RSS until the container OOMs), -1 = unlimited.
+ LlamaCacheRamMib int `json:"llama_cache_ram_mib"`
+ LlamaCtxSize int `json:"llama_ctx_size"`
// LlamaNGpuLayers -1 = all layers (Metal/CUDA), 0 = CPU only.
LlamaNGpuLayers int `json:"llama_n_gpu_layers"`
@@ -1884,9 +1890,11 @@ type RuntimeConfigSource string
// RuntimeConfigRecommended defines model for RuntimeConfigRecommended.
type RuntimeConfigRecommended struct {
+ ChunkMaxConcurrent int `json:"chunk_max_concurrent"`
EmbeddingModel string `json:"embedding_model"`
IndexEmbedBatchChunks int `json:"index_embed_batch_chunks"`
LlamaBatchSize int `json:"llama_batch_size"`
+ LlamaCacheRamMib int `json:"llama_cache_ram_mib"`
LlamaCtxSize int `json:"llama_ctx_size"`
LlamaNGpuLayers int `json:"llama_n_gpu_layers"`
LlamaNThreads int `json:"llama_n_threads"`
@@ -1898,13 +1906,17 @@ type RuntimeConfigRecommended struct {
// CLEAR the override (next read falls back to env / recommended).
// Omitted fields keep their current value.
type RuntimeConfigUpdate struct {
- EmbeddingModel *string `json:"embedding_model,omitempty"`
- IndexEmbedBatchChunks *int `json:"index_embed_batch_chunks,omitempty"`
- LlamaBatchSize *int `json:"llama_batch_size,omitempty"`
- LlamaCtxSize *int `json:"llama_ctx_size,omitempty"`
- LlamaNGpuLayers *int `json:"llama_n_gpu_layers,omitempty"`
- LlamaNThreads *int `json:"llama_n_threads,omitempty"`
- MaxEmbeddingConcurrency *int `json:"max_embedding_concurrency,omitempty"`
+ ChunkMaxConcurrent *int `json:"chunk_max_concurrent,omitempty"`
+ EmbeddingModel *string `json:"embedding_model,omitempty"`
+ IndexEmbedBatchChunks *int `json:"index_embed_batch_chunks,omitempty"`
+ LlamaBatchSize *int `json:"llama_batch_size,omitempty"`
+
+ // LlamaCacheRamMib MiB; 0 clears the override (falls back to env / recommended = disabled), -1 = unlimited.
+ LlamaCacheRamMib *int `json:"llama_cache_ram_mib,omitempty"`
+ LlamaCtxSize *int `json:"llama_ctx_size,omitempty"`
+ LlamaNGpuLayers *int `json:"llama_n_gpu_layers,omitempty"`
+ LlamaNThreads *int `json:"llama_n_threads,omitempty"`
+ MaxEmbeddingConcurrency *int `json:"max_embedding_concurrency,omitempty"`
}
// SemanticSearchRequest defines model for SemanticSearchRequest.
@@ -6783,508 +6795,513 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
- "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",
+ "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",
- "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",
+ "4NVGU+AvBVMNYaKrYoQTJihjUkInlAttSBxmHA92dMnhWJsW91T2wxIvfEb74a1UCbsyMm9fTEJFwrLG",
+ "FIbytqaCUMj2IRxSiBKmNTqLiGZacynIHdWYqkaoSCFyip8dkLc00+47Qpop6JNUl76mrr3hf5aj/l8K",
+ "VrBIJPZmL3LnTFZUgB6vGSPxz3Kkh/Z3xVKI7DbmO1SfWl3VibVtrIjJmbDq0b4qhLDzSDIp2BAyRb7G",
"2eE/7OfAORyJO6YYSVnG7AkEb6KdO8wbNGnQsO1LtalVTTF0K27iGOd6XQ0fhc1aWmXT5rsUngYK2IWS",
- "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",
+ "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",
- "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",
+ "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",
- "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",
+ "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",
- "qoYZE6km8TEw6iGpgnDc90X6q5YiRsdXDKPGmK8ZCcsAis+4oMZlc86p4lQYVx7v8zqpYsHGSwnVYPnN",
- "qTBNXqMRNcl06DNDVvcGabjutypjrD4DIA9DPBdBB+TC/Mt3jfFh5rfAcwLkOEASUDjCQxi3/CfiSJT/",
- "TiVkCOFvEHXsdaaMKjNiYD7gkt1T+ECTejmmdT2s4qSGT8Mut6ff18XfDiJv9dEZ03rnZJ81SoXRD7TP",
- "cHc2niOfEr3kzna/khyvPORxTKvo+ywKx9G+As35cYGxj/AUTblVDHhCs/6YZtmIJrfhLVBZ/avxEoXj",
- "XiTc34DWcQ9qmuI6F8dNh2RXCeirXIM6sKSMSQ019VYaYDYZloU5DapHBLtj2qBf+8jFR78dkHNmNKHk",
- "5iwSeirvSMbnEL6+oyolMwmgNmkBpj2FELQz99GhHIk1pNu1VpFlNLdcW4kdl/wki1HG2nI6d7nIHnCX",
- "VDZ4i8KAKdU1m9NuCp/bNffW3kFrjtfnTaej/aLN3ROb9MbVw1a7RJdAe3iasRiivkKGbCVQ2/dJYIOi",
- "BD0bACQf5ibFLvUIX8Kzan8PtIn346A0x/vxmHL8H5dThO9nVJu+KgTBOaIhErsMo0LouB4AsBOGki+c",
- "Q20rerUUIByuA7thh3uUcfl7OWrweBjDZjkKzA1n3s/xUd7hh/kB0yJnHmRi4xDrvNvbJ+nM6P1we+Lk",
- "ZeLt9iUtl/QOy1jc28iLUJ2Sshy0KClIbEeLB+QSayuo7nNNuFO5QrTliKRSvDCEal3MGEEsk6IV/cqn",
- "gey2EU5NeRQDrOrCLk2vwuX1A+EOwS9rYnRbeF3hkV6pTYe9XdrqJdps9Nj/Xo7We89+laPtLWZ7Rh/h",
- "MoOx1vnLzrm43VT27fNHmvOzrE7jcrTikFoSA7hU6SLxqYSVQv5IKKZlNmdQyW8kKRN2oPJaaKYMav3d",
- "O1/aOuRpD0q4QkLLHiQUwne9mwZqfEeYtwW7+8MLNw+XWzSj98EG/Zd6HvG/bJtMA8RopKiccHEuk9v1",
- "snUpMut+qdRZcQGwG+CBy3gK4QsuUnnXDE8W/M5LiZPyjql+QjVLEZf0KFTegO4IUetFzkjM8yE80Ozl",
- "ZPc5V1bFbzAXL9+efPvtt/8K0ir4zGSWWo3OLZnQCYNKathbSnQmDRQaQ7relkHKBkiaK0TNPLvAsLBM",
- "bgnX5JYtIHeluZKgzFRfZuOE5ogJYRTPc5fHYz/aTPLmhICY57HHKAIkurMLAhieUhia9fUdYzmB5A+m",
- "SHdGxQI3xmkJUrBIIBjo3qCyK7VPds8uevjWXvgUJBkI5jNLljSM3OoX7lublQYnG+GtiiBE0tWYYe0J",
- "WC8HLWG3F4TlsXqEOMQh18pDuc7XvkNwZ2tQopb6/7UAQW6WbZSFANIGet7oBvosRZ7KAd+zNcBXhZkO",
- "Z8xMZVNOHfP5HmUeiK8tNZLoQo1pwkjUyeREFibqkK5TvveIVNaCSwHRq+uwrlx2UwkC9kKHGgMjSSYn",
- "IGXkeK9+ANxHLT87yK8m9aEMT9ZX8UfO7vr4I2Yc0CyDKEAmxUQTI52pWl8n5i6AIRp1IOfWThE+E3V8",
- "9tQdN1OQiy7HmChZiLRvJZC3ZiFfOxKQogCgrvgNfYRgFdrVTYA8z7iGpDIOCWTEhcymPNeRAMy0bsDP",
- "g4/gCwjIgshEp9dkH7+/t0NNbmuk9nG82KtxV9igRhaVKctaauebAGTevcWkorM3DqapkuOe0MTuJFcs",
- "gVypSj0sJgkBfmtzmlhzenLIywcFx2V/DyaTYowBKgCG0bfNEUb+VzYcLQxr9inu4BgHxdelAVe+2krO",
- "5mpnoM4w5ar5Fj05+9Pwp59u3g5Pjk/enQ7fnF3ipXpHNdEJFYKlnrOB+SDXJlQAk/B18oNl9ZJGrtqo",
- "GSnIznb7y6TCK5tSHtyXe5VVN5GrLCXdteR1fVnrb64KtVyMn1wTOS7KDOVlYig5oy05/JdoEKQEnmKz",
- "/kQCFhJD+OryPIbKq2BXfLg5Pw8VZwBeVmxXE9nzU9rhlG2uyEissscFUy0rvbBSoKLhh+dJV44NE4T9",
- "pQAgiNIqapY2D3KfVIC3Nqbb24fQvGqE97I7Ua/46qEjAisLyodCiZkUTA8qxp5DZ6uibEWiW4JsWYU3",
- "oFOF4fQelnU4LDpXnAxOYssYLcmhawDMgoBeXqFLTvbpzS+0n0xzBhkamkMr1BDluwFlweM0EHjALX3M",
- "AZME6mqcFVUyOAjPjGoTia5ie24Ux/ZSWBvYAfLlivXt7pNU8bG1ZGhya4dyClMkKvgclnc0foNqEnVu",
- "sLtG1CGKooY2pcL+BN9aW420Wsm8Y4QYvHGeeo/xJ+0EGkfrzFuWqaL9iLSpsu9+BpWIg11m8uR4bBtH",
- "XgFdXqrxBxDJqbTKjAeJYUoD+lcXCAL+ByDD3pKMnTEqdCRghKyiiZcVVi6TyFLt9E/Xp5cfjs/LctCu",
- "mUrNAuaML7WwE2Bqr0fupjyZQkAXdFusJvNlNQhB6ktDQN+FUhQKJQKooGNx25a8uqbssLmlz1JHH3TQ",
- "IgbuzeV55SQPdmq6A/glhouJ3rKm58o/bl/9S8YN23SlXv3Pc26lAjV0RDUrBbMJMUkUR6VQCZLCiRZ0",
- "KCNiESRi04ll2LHciifdNJ/0mrWctjXJ4Nnm5IuQMFMJwTj+Xxsxf3T+b7UWr7ycVvSHqlSt8IonQMWL",
- "3ZY2vCpgl6XEGjVuvR/Hn+2tte9KXdpTwCKE8df5dZbPzapr5z7JihSOkT20O95eM3o/xMSN3RFHVkZe",
- "/ty69fgDsGTvum0O4fj1wSpMuioTX7Z5eqdPo+mhdyRMdaDe0pqWJr080DqSFbMZbXIS1JTDp1Jrfjs3",
- "DKZfVLdiq8N6Bc+32MpVYdqQypcPvcnGd0gSDRCIbfLht8epbWI8iOWq+K6z9Vo2XiXiyj6u4fQACNvi",
- "Fdu9uKEah2vc8/KB7dwMtQ+uvL6hXmF5mc3eqvDNnS+oJfpt8hRVBmqa7SWjWvOJ+Ghv3dYIwwbN/QO7",
- "88WlPsoJ8C2Yy98jDgECaic3IzdvVgAuGWBVJ6wZ06ru2ipzitxLjTpTK/KYA8lAL6AHAAql35uwiKtu",
- "tObveo9KXPq2YtJVbOy7WLjaQqxp64EjSVExYbqlI9QjAPue1FdXxfHajB5X9+BVxtkAAhZY4YEowqsI",
- "Yd9/ebDgyiKeCuurfkS+INTXJQOV6FQAGlW6plwr9OWqVwRZS1vhR8g4o3NZqGpDwDsA4yrEAItboBhK",
- "M1Opi/HNFAB+J/5/7FM/YAlMt/Idh1jlk+xYOtRTGvtKI4bTX256FnORKDZjwtAsjoS1/DGjXArW/1WO",
- "XmiwVvspM0xBhjiXwlWLG+esjATgd3QxPDajC+IyTqwUgBAYNXaBAJVkTWFIP+3DLHv4ctZHrHjA3PMt",
- "Fn17xoRqppfijFCNZZXVMPtGIbg7gseq1eooZ+9N14Z2+BRZgZdMMxOi7ZtD4S3tMLGODlLVoIouJDIc",
- "ORTktNztwe7pHh6BSia3djeBx3ZAbUDm96kT/gNw6zhgU/i8m38dd+Ap0ipaCX+jmdrY+Gh9F6PmDghN",
- "3Q8AEewpmx+s9Hra0OjoEluNHlfqBlaEMVxoza0M6V8KRs7eHJFxYeyBnDOluRQazrrzU2EvSPgK8enG",
- "gJrpIkg83awnVWbRuAoUISeh6enySal1/VwbiZaK0LZABAB1lOHQ9vAD1ocMseqjtJWWIAGV1LoPsWd4",
- "vA+PE83/irIQUeMCYiN+BmIx8DikQOwNyAH5AbKO7D/hVz+19Rc7gMMP66U44Y1X7W8k5n6n58VwkhfD",
- "jC5cuXKdBv1X5AdiJ44PkO57Zmi2f3Lz5nivB0s7ubhZ7oLSMIaZArT66gD2ExkzBB7su/uQFkb2EbV1",
- "M6Fm1G0nMFAiBSYIJovNFFAskbMZEykerLU6TJWDLyvv2YsHZO66SlwvDdMRiMZ5pz72L5uQHCAsAxUv",
- "rs+ib9wllxJqEio8ojwlUefNj1GH7Eci6pyKuf1fEnUqk4ccmyxDKWckQtm7joN/YAuNkh6DYpVyMoTO",
- "P1zt1tsjcZ0J4x4ZDFryqeue4aZabKukINmH3qFLlLwLkT5yp7gxTJRwv3Cl+p7G+xUSQ7yEC8LGY8dU",
- "Dwud+UmPFk2TloRrXfisSDvDi5vrHklobuoIkS6aUKkb3w2XeFlgrhz+xtO9ehzXnZ4GEbRGeoZTsFH8",
- "X9YP3cabYCcJvp0c3UZ2bisvt5J5O0qtTe7w39zub9z0Gzg3TZ6HzJeoStddaUCuGGRAY7dFI61Jta9Y",
- "ntEEU0HknCnFU7iFIwGBN/hGD7vsxVEn6sTWqIGyN/z8npUR8UFMuqKYMcWT8HcjI3Fyfnp8Wf92F4Si",
- "JRRURGlo/AdCUszJPqnIFmvffHQFv24tt4zlLsfQpatXG+VtZPnN9TprjsDmmGDTkdj2reoR2fad5SOz",
- "/XuVI7T5pbVHatPrTSV6V2xGheHJBmB3F51qgunKaHILGXyQo6BkTpzbgtxBkB+0Vm9OUVE2cVZEe4z3",
- "wU5FmA/NLWnsU7WMF3IPvV8xn87al2/Pzk9dpi3pQlYZcOGeazZcKLGF+sZF2QKkudlsIjUXjGg+4xlV",
- "3Cwqbf/qIMOkezB4bYkdiYxPpgbRgzE2b8+ktraDUTQx5MM5+UvBoCq47K2CsicSVlAY6UGej8BOJPHB",
- "4LtvYhzVKJ4YksiU9dGXRjQwCdORSGjGR9gd1j57IlN2ScUt5DT1/+fvlkCgW5MVA4bEil1uWOApEDSu",
- "befzMlZoRfA0PQWWz1abZw6+MAQdcNZEDZpl/QR8HPAk9LUWyaKHSeLQ9pW8IilL+IxmBO6QuvbXWpj8",
- "kI4G1W43z+Tn7C2RpJm4mMr/JJBy9bqmR6Elcj1012ELtLDPtvHVxQlVaoEoS4C4DRK4Gb4eEeIZEztN",
- "tHxrl7b/8MJWbf+bsstrCSgV6i6toUauNbu8PvXEUXKHaLbjnUfUEIUx1/nmr6ZUsWu5vsOvR9PbHOEJ",
- "TzaOxVOWUHUVHMTLuSDDMVwX6xJRsaFFynIzJRRRiWdyxrBthqazPHMidYNbp1ZS3Zyl3wzq0eTgO/AJ",
- "3SSZ8gyKSLENiSYUkCC6WAlM9kPDgr3NcwRPeTPKSfDztZMMDvKImTvGhOsgaEmEMDgad2Lf+xsxX1Hn",
- "9E4QV8DcAqSG/v16FDXUPsPHXEE0q/wjgAGsgYK3Rr3zMQTcih3kM87KE61XYaZGTgQWXFMpBo6Bofcq",
- "Dn0l9JJnMuCy+T4tVeQESHXevMs050PnDkYt1vKwfWb+qklSWvOHiQYe/BF/qOZou754E9nSB2+jv/cY",
- "/SOhL+OYiwlTueIC1R4r3JnKsJ3iu2Iy4WLy1pqH6JNNe5EQ8o7Evs3f4OxNdy8mOC3su3lY08sACAb7",
- "cB4adm/6YYr9b/t6RjNwbmF/zkP8Tx+0v28PXx28/u4QtLh4XZ9H6BavWOjUBL42F+R6oSuhvzIvPnbH",
- "IzSI1IZmrI8p8SOaTlhLEn9JX0/BzSS+5SI99MSxi0VqxOBccyuPV4H/wlCgifOEVZujEHedj+ktI2N+",
- "bwplFWTQQLkpDCMUlO8r/6rlQIgJghTYdnHDGRXW5PGYMJt6LNKlpUOON5Tihz5OIE4j0S2LzUHJ9uTZ",
- "w3qPsZSIomx3SBNKJooxsQ+hXCt+hf1UKo0bGrAlsbo7Z2pG7cWLr8BDIdgXie676+uLPgwZGlpCnyAr",
- "6v0cwUDB9PdLK3yIi7oChbFolJoQgbYiDjM2HNTLwk8/l1nW2jfK6tPaVAXFUvU8/O6BpFNvj7nnSdcX",
- "b0KsEH/cn8fOHOlFAo/kweD7wStLVWgeUwjDM2QcSAAse0i4rjMO6UJvmcMOB2aYSZq2dH5xlRysHoVw",
- "dxXIFGU0+IIWkJfPBfn+4IDM7AQqnbzgiLqXuCb+HrKnYEo1SRTVU5ZuaN7SxL1Wiao2zwgdXLa4ymFf",
- "mptJOJx/9wzJ6QQzKKF7VH3j40ofJFJgEcx2Oe1Ayyr/bAcP255LPgyoRJvOuRu0n0xZchvk07RsylZy",
- "5Ms4Ep4OMrTNQDM/WwD4lqv1cVEJ4UUedPd9C2ERrol0fkB7FSrmwXLrE+FQj03uOMiJrmbGtZA9vbw6",
- "+/hhePLu9OQPw9MPxz+en775AbBqq94I4noZtR5ZN9oQRtuk7v8RHz6xzzr9uBVN0asAK7taVyaWDlyv",
- "wV9dSUhv1HgaVac7bpLpSgffVtshCcHmdfBCK8M8okt3cydlN4/GJVUyh5+s4/y6pLrtsuXW9NVfl/6G",
- "q1nX4HN9Ie9zLHhNN9w2xHqrpQkzbO/JwCeCWk3mUYRcST/cgrQb/NB2mEe7fXfoSfpEjsDa0p4q5XCF",
- "F79g1uE106ZBTLUtLeUzJpqVq9L7EB5C7FarkJR2Rtmvf2yYisSVVTwG5IBU4bnxiYxRJRwKR/hkRv/K",
- "fQeo9XuPjSc3IGbXbu8KWQohWIYNk5t8MNa4aYHk7nVQw9+gvflsSlDl4XP7qDK4riauV7MPwt+cNfsZ",
- "WkVQ1ZgKhXGZLNJxRqHdtJioFtWlXf9p6V8PngRPknL9mwjbnOAPK9+lwKS2V5uy+sPX2yd3Qg3N5KSx",
- "Ms61699tal5V2TC18vNr5taWlFa2ilzlkakrS26oP6AzlvYNfJrk0K6N+KcJFO2nkJcLBuVeK1RHlcfg",
- "JXD88+S2LVn2oZyJ3Ro1M+1ni5JECgEFNpj6j9YNZKh2Ee3f913baz5S9VyfDVk1G1Jf3Lb0qkfF9V0M",
- "u1Jd1qadL1MTHrj/v4H9a+xn7N6GbK3AkUs76SKpRoaUhUiEvhfwxBEmVUSdqFNmAXPT1i+whdRtrvc1",
- "thwa1tYWLV0gEMiFNUlDFsyUHqNam/3lsNB6f3tTxzqgWdSxpnuE++aa1JW7chSMvfVZ2r5lZ5tHv7Eh",
- "9kO5AQXOut6RBGrbrZnfFZIYRTn0+dIZ1dM91BgyPmetjVxqnB3c6mDmWMZCv7v9wnoY0tLnvhUKfPst",
- "6f3vJSe1H3irmbnobEPrBuPS9RssDIgpb49Y36wmLe9OiyNkmDie3ECDFjULxZlrhXq1EEk7aOnTdHwm",
- "3YN9fxJW2z5DcpTIFoCKwcVkXDgnkl6IxKGWQfWD82rEpEszLbFghmqUSKG9Mh+HtubxUlpWo1ekVigV",
- "xmvILwdQnBIf1YHljBi5ZTn0eLCvDyqD25XmhZ72U8XnTESiC0nL3knXg9ktT45I4Z20e0eVJf/9b/8R",
- "CUc332MaOksTv/IjEmOnVhzZ1yRJQVI2oyLFFOxanU3ZndiNg2pkQbcow6gSa0smaz5VD2jdu1WLY/sQ",
- "oSPvWZaFSeQMfeHgUUY4NaRAJCpbo7AMyoVFfdFUoP3qnm3TGDescg2t1sa5N7Xk9KrGJpO7ZfBN8MUP",
- "hlZpH/JGr3HNhWtjUxt2KMmx9pyQJJNigrUBUyYMT6hhA3LJxiApXCaoy7r2Nb5YrIPihoGWYz/dGuqQ",
- "Cc2GHr14tznO6AJh1bALdB3vC2A6sQGMP7iYkuVQg9D5AcwYKqTxS5ZVtaGLSFDsYxwALSGU6Mp279ks",
- "NwPyjmqgE9XGtZifFFS1xht8b73V8mn7S0jFgTrpKp1JM5ldTNDRObavLWH4b2je185Locq8wlBLMWhp",
- "ptVWNz6XGAQCXE7YAAPLFzJG5658zeu6kcBs40Jg7Vc6IBdUa3hLuIJrn08sFYkrw8eoE+tIcDMgsT2q",
- "cSg9L0EbgTpOb0mb8oCfTwbopr6ND4TFazkTZeAl9g8NqfEFsQPyxkeE7eZre6SFNCCYw2GGVtyhIO/j",
- "JTm+OCO3bNHGv5VxHg6FtgNccVuPhK8jNUh3ZHke7W7y3cG3e0GOyLEVFxCt7C+hy+s2IaPsskUQMwNy",
- "3CJmiGITqlLANYMKU67JOKP2mnyDGh8EpiFqdVS2UHTbjjznC5vxZWxkhbByRsm0SFy5DnzDqoMwpT04",
- "ePZyhtY2kE5h+Ihn3LSyiD2FQzzQtWrQdmG4faPRJ+lru9R7tHG+6xCrKvZXCx/+0iIQNrVKbm08BTu5",
- "tbfODvUzN9MALbbWXYffXudbr3/v8FOHZtnHcefwz9vAJ/dasq18vuIQqp0bfCn2z5bbQZpDwmbqU1R1",
- "ibTqEUU2p13dssV2gyk2l7cs9aJQA7KJ8/tvPSK4QKCGubHWrtp5KuTderlgWVkbOstJ1/UwsAYWpALw",
- "cSnIyrafmZxMWEq4WEpz30EqLzFF8yatEHKVW3753Os0hLcbIFpYcttSiHhu9RwwfCuUuLk+6ZHLtycE",
- "6YGZEUGm+awV+9bDCw0r3sf2gEfOFJcpT3y+AkyUa5+f0OwRC86whpXCb8T1luv5LZ5VOASGQOvJLdyq",
- "qJig84A6RtEupX5G621d5Xsu23vk7oAF0es4vAWrkz0WHMJN+0yM5dqGAHJYJvRsSmTxCUkhDypbkJq7",
- "wZWclgavSzK0f3S8YUUI+CiG7inAISH+sDPyDbk4viZTmkYCbr9DoK99cm9AQHvBtsk1rF9UdP00CPoX",
- "stYL2Q091CxRTRGHd++PTwj+OCDXdl6EWg1SaA4pe1adV9JAs0vQWnJpGRMW0Oix9AM2OkTfWva9uTwH",
- "a59qw6wGIh3BXmhPToIZHB6RPadmCql53maAbTo5+9Pw4ubH87OTIYDQaVIIawvZGecKWlORBcDHoB8e",
- "y7i3cS5Ul7BCwd4KK63hyY8wZgOiVvj7KlR9vuQ09jRJWcYhifDm8hzVxFHBM+PTWyPR4F32FERDEYFK",
- "uCZRR0jBok5LvmdZXb8iCBUjMU4+rjRoGJAYiYwdYSj2RHQRVEf/QSTi0htbto7xfN23e7e0p10uxoqG",
- "NmHQZMlac9oTyQHvgkp6RKjfapfvJRiDYlES2+XGWKUmpH/Z1fVz7ditUCztES09xcFoegFGpaO9NyO9",
- "hMPhOjU3cw9ou1mgORZYW8XtuOiSJVIkPGMf0enWnMTuksrd1JzSWuqydqRb6CC0PkzQHjypNLjaKqpe",
- "Pt3zE9xmkW2OzVElq7INcr4lzQlX2/ib82Nur1+37UkTDqWjd+PA6xR+t3ebIxKBJmWtWrndJQus2fjK",
- "Pngb9Yk8F3XnytI9oBjr2+/UIPScsHJeH5BZdKSZMIN278ASiPj52Zt+xm+tWAEgnjo0aauHZ+krgtt3",
- "S6PdPtb4/raY5t4MB5XBfzaAWvukVanyKXXWBgU1PxLQ3xyI80eu+Shjvk8LDN3zzXSwtd2iijnODeE6",
- "EgCclBIjXSI8eBm2TON+GpPb5QdUSbPOwt4MAFkD13yQSf0A/M3yeOyAubnOsA4frPQlWSoYCa6nzOoa",
- "qYOIoiUL9UjKEomVzwBfD8ZD6K8UiWBDYfmEb9JN3JihTXNowORH5GIsI9FFtbtHQpF7jyxhdu+tQuKk",
- "kmnxwkTCXsBLTaSgh1SDL3Z33Ndd4eqaL6hNeK7Lu/TEsOMrTPCIItCtMMeXB3wfmOUpsHg36AgbwXrX",
- "I/Eu6xRb7Ru6aE+mhbhtzFj3uK879kB6BM7qRiK1gDNc0rtVYIZQ92mPIBbkY4jVqrV20aj1QnEG2E4O",
- "NM3aD9rntA7IBwmgdP70j6TUeH3QPM84S0mXWqNqzmWhif0vOK1mRWY4/o75nouyDyoswvfOyJgBRDYw",
- "H1MJ2JjM1URhgm8kAOVhKpUJKBFwkefgoDZ9yOajiZJiMfMAl1+4/9QS/20DcYtbWULdbsGqb0FFu3Qh",
- "/Oaut62soxjVTQrXFRA2oYZNyl5VzKdGgDEWzyFLDbIMMX8LSjTtUZeFiXuEmWRAzmAdEOrLMJQC5Tn0",
- "ru7JIlq6JBJBM4KZPto168gYvT0iWJlT8bVkcoIcFFePfVzO1V5PMMg2NvzSXjm6bEH+C2z0/ED6t/V/",
- "d/XDtUOGzw7IsVhgr0hZ9jfytalxJKC5TS1XZkrt9Qr9uRQfFQYxNxxgA15NdTu1bF6dZFJUWonUarZ/",
- "2ZGm61xySzRt67w2mr3+vhWLhlHhIVWNzPsfgMt+fP/6ewJvaMKX+lF1NZ+ISIwzbJQLaefYYuaFJnao",
- "LigruXS+rR+s8DRMWWlyhfWbaTPAH/TwijqOxHnoEZpGQgqSccMU4HjfWi1+zlRG86hD5npAok5uD5h2",
- "4CsVye3dL5uFWMqEZruRafme4CW5gogekGs5Qdc26I5xuRsx+hzNnYSvQQFOpj3yF4Nwg5Ekrgn7eNv1",
- "tPSeQhk1racdIeriSjPMKiu+0JGA7AjNJgAPgcXwEVoS+3a//gfEWjuQyRV1Kn/Za3GBiWI2nPKm0tAT",
- "vD/dTCrcB7TRhZrDTH1XZPdrJPAyTmgO9/OMYgtZICO0psvkiGb+dgZM35a0uo0yqLYpDR7fxUhxYOuU",
- "pzRZWL7480Hv1S/BJfe//1d/lDGRWrayawC1IhIzLvozek+E3eCM/5WleBrteoBFPZ+Q7v/+Xz8cDL7f",
- "w1xCN5++YhmbU5EwMrG3v6J2pVb5sJZJ1LmWeQiaR51I5FQA3qYyOoTfKpBum9hsvexCFlymVWXfe1XZ",
- "VD+CWwi8dguhRI3bzT6oqrENNgKKcGxb1gSAncuAU1W5gfDGd0BaoaIA8m/sPRuJtFDLWEDudCVSqSI3",
- "1VaYrl0sVupDJ33jBVPpSuG+MzCdTBSzjJAehY7LMI7VHGoBj1uBjQSZUxUh44lbeebL0nfoCdyubDU1",
- "ysF7cz1Z4dxj12XvmymXOyoMuWOK2fsajpEVapFYuDgFQMcTqRDUuLzYcVkp6VaTv1zT80jgZiOduQqg",
- "Y4CNT/OcUUWkQOTGBbrIIxHjbf2D1yu8s42PgxqeS4hkMpouHk7QqvrURNE1Jffl8QfZgH6w5SuGuIta",
- "O8sCtsaqmkuEL80hrn04FcSPAVeYJYPdXSZMJOTYfYyLlM95WpSC2E6ETPlkapkZZXT2GOq0W/mANDIc",
- "G70Ft1mOCi0hK2FwEMczjme3G+Ma7DfjPYDwBYv5EPjihWIlQ0IiGIi4SDhhMHJ5vjqnSjMypdnYH+Yp",
- "XiDctUVxNl4krCiguXbeJJpNpOJmOoNYX6FYH++IMRV9WRiv1tshmdVjmR6Qa8UnkHFaTbcG2BYjIRFp",
- "bFncfv3t9VUEnWo9HwPDIyeXTAA8PaWajKyF7L5plbYiALwIdkdwsx6+q1d2695eX7UxfZtREENa+7//",
- "Z0ASHMssk3cDEgNh8bdyNWgWp2RsNbtRYSLhm8y7bhYI+RHgHWPEYhyQ2DWJGDpzr4yEeS73kt/uOgUL",
- "TVcMdrgMWApN7PFGQAntt7KrGSNx9QqKlzpQQFI8LAoahNVms3XsvwZisAYGd93u7GrRrdMiVscG9I2k",
- "sNr9lWUVZ+TYzVHHRWOHX0wzgixMLL+I7YNS8b9COtAh+RHeJlFxcPBtcnL2p+HxxdnwD6f/Bn9gMfgY",
- "7FCdQzdQqQpNjck7nz8Deu1YNmiC19cXkKXgTew44fcOiyUuTRZAXcLjmFI2A5ggbM1wxxWkLc8oXMij",
- "hWF91xeOJkpqvQROo49wmAp2RRwJTBHmgsT7NOf781f7uOExMdA6pSKrM1dMHNfhMGKIn0SChmxF3Uft",
- "gBqImbj2EhkVqYbZ/7f/Ro7LTFguBSzpTpKcKpplLIOsd8g88AUvVhjSWaUpNEBrHNoX++Tlyx+VvIOU",
- "y/3Sdnz58pDECMPmVma/ug+ZYTEaXZCPSL6JBCkzcQHSHPCR3hmTQy1PIuUtxw3yeVkxKs7uF0j7tZcZ",
- "gTyVGbULywBFCfBHrfImDKyg7wLfTqHTA3LlM+uUzDL7ibFUkPP56juS0oWuNBuhOgAcDXDhJ+dnZJ9c",
- "vfkDrHYd97r8Mce5ds/cvWVPwB3VdmQH525v/jrhct6/ZQsdO5x8yDm39l1fJzJ3hSXWVB8x+xmfxlfe",
- "6BkCCVl5RQGSvvS4uIbdwBiuZM+3+AKUXeQFLwf2Dkn80+k12Z8ymplp3HP/TGWiwWMG/wLskpwPFnSW",
- "hUeqTDCS0mijaN533G5fbeMVu0WYUQ+4Ncc31++Gb86uEK8GuxvpW5674i10rQXQqdCurJuyOctkjhiK",
- "lq0wW+OOKgDX4dolE+4BKX5eToYy1Npi2CbZp+hjmjGeeW48kXQkYKI/fvx4fXV9eXwxPH7z/uzD8PT9",
- "8dl5TL4hjb9eHF9d/fzx8k2MEOT2oi6T+7C+ojuWKkF/lzvT4dRI4Z4Eku0NyDHBvthuLk5uxmA+SEEo",
- "GSump2X3YWtSzBzOg1WWiOZiYrX1mIl5P+xX7HNDq6mh1E3QCxcfX6NpqhhUNQBzub/GoR9jjCat9s32",
- "MW+HOT0Pk/bJqBK44yISN5fn3teh4e4X2QISV7yl7Y5EycSG3jJCSfzJjvk5JjeX59bAVnTGDHNoza7n",
- "/cuX48YeoPFSE9D45ctBJE7kLC8MbD36kLzPdz8gZr2jenphl+ppc2UUozNgOOeDtD/Ued+/vY8z3ses",
- "fOg+E5OpFLJQrhsSZivGZMpoytShVWDBAvG/HBIIYaCU37/vi/RXbW8MDeBGLBiWYK9DP5tICHaXcWE1",
- "VoBrYSnRMGegw5mdyoXr2nM6Z8LEBBUA3Qs9xOMpo8qMGDWxPYXCuLP46sAXcQ7Ixyz1osc5j5hIiZAE",
- "Jx4JXBIYgXF1EbCAPTJhqKIjlztu7f/+6uOHqhsYSH5qNTht/3HsnejhGUhqLq+3kUwXRE9pzg5J/Cly",
- "VbpR55BEHRTjzsWPYjzqfLYbW5OInpWwLci9XQyXIriXCoHPLcicKm4tshIuKltEwsek7ejot8fRB4OB",
- "G82qONwAdGapsdhj2angfnTmryBFAwVx57Dz7eBg8G2nAvMdBK09ufsl/mQNImPSlDV5CQqzBhXZWi8L",
- "aDNUx4srQR6RK7jxiWaRQEMiFENCdxdvXjExt4TRKE5pivnuiWKgd9BM9yKRZ4U1gH1estSV1+zNWIIo",
- "OmFXinHXVav0bxfa99UCDPlgGcHMUyXzVN6JnnPlMdWHv1ulrxfmH3V88t4fP/7b8U+nXtZ621TTuT3l",
- "nUiMqBCQE8OsALZC1JrnHCQkbiy6fbgUZ2nnsHPOG5B9sOOs4167Oa8PDpaiucvHBWougeibbLuV0QDg",
- "BbTopfRtjv6thl2HuovvDl61jRUmv38DBVlWYcLGRt8dfLv5pbdSjXiaMgQC0r53NM6oMp1VjoZN1aSL",
- "lyngodizRCe6LMH5xX5049HYxyKBjSfE+cE1JrCEeWjmvYZd4KdvCMgtfz5GmRztBfaCbNJlZNkKQO2g",
- "rNagilnxbcVBGLXnYzr4dWI/HhJf7NU+p4p8OH5/ehWJoBVpOsZOKN41KZ0G4iX2nKkRNXzWxLU/MYNg",
- "ryvM9Jyc2zZkA+8GOONlZNYvxri9zvf4Rgv+lg7okQG50XmerZg5ff/j6Zs3Zx9+uqqjNu4tnYif3BWZ",
- "LK+3BNENLLnhUPQ6edGUo2XkjCdOm0CLzNu8NOMp5M2D8C1GDrEBubDn+ROqWEG8CqLvKOrwyPIepFfa",
- "z3VTRbk7UIiMDnoaoj1A7h9e69Ua28p5M9aMK4SRRYKIqJFAeEfwH4EqratQvWuO2xHqMiGw80KTRijl",
- "EcMMcfBslncduzeRQMf3r3IEF2cIoaH/z+reEO13HrtIdGP7zR/sH3sEdYIfqq1Qvb+pfgxb8Cs76NNh",
- "2vwoEXX9SY7fBrTMz3VfkjXaPq8Ig9dfQxjgxB1wPEuPwCcaOMcahVbQwxk/2HzGf6ShR+hXuQTdaqAx",
- "ZcrH0AfZPOC8b3UJfrKX1+d94zEXZGOpfMGhSp6YqZJ3fXpHFxWcde9ZWRUQCc0y7bAFSdeeF0yPwl6X",
- "VikEB16YzREBVH+su8fuBADSgHiqe07GXBmZW91yQKpXNLZ+ZGkV9lCkhIpIyFssNyY32hcQ+zmXSqRT",
- "9OyU7fgXN9fROrXB38iutytDbxcWCWCEDIVZd0RTe7H3yJ2SYmLt1p5XF73Gu1eNgFgJ2iQKGhEi0ReN",
- "NrCGilZu98uFiDEnzEOJ1k9ur3IKHwJb+8vDRdA6aN0lGCG3zL7OWcLH4MktFaAu2HhggjEIsNuFVsHk",
- "glN7C7n1dErMeijPJlUGUbAIgKJDvR5LvRj5bYorq/q8bipDrizEo+VUqgqL3DkhvGLUtdOIhGK/wtHt",
- "EcHMnVS3pBCuLipjkLUHl2NdSv7RKSfoyFsxExyjeIepU1Wg6e6OUhOccf1MJrd6K1uBi/6MzaRaEPcm",
- "OGCUb4tdAaSr6HQGkBtATcIWgGaqmJ7KLA1Oh7MLRGLonl300Om+R3LKQdeAkcidA6AqKYo9h6GrmZB3",
- "aMV/9/pfB+TKWNJx7ZMpoDmA/bpm2bg/ZTTThGr0XuqMw71zx0Uq7zTBanZnk3BsHEPATdfnAntka0Fz",
- "PZWmzSAOjdCf1RAOo9RS3htOoOtrgXv89Qxf6qcBsQ1qGPIOzusxfGsnwNbc7+d8bEp3MbZ/9/mBEMhA",
- "3//yZ+OBb+We8+TWcYtj80MS89zD3IRcsrMLAhSTwtCsr+8Yy/0LR5Vm9la7rr1X5XnfXt6ytlUpaAJ/",
- "QcvFX8cZ5GAJNCVoYqAHcK0hfSUsgNa2G9KF/awtPojEWcpmubSseIgPoG5yyxbB5VzCTFmisNTFojV5",
- "ffBdE/9DQ/rAms+kytcH2UmD/66BPyxD+BB9VyriquFdcdveF7x4vnv9epthnEiD2rD6STsBDqDNp2zn",
- "QwYmXPu98DPNbjWGin766ebt8OT45N3p8M3ZpW/T0+SErTSjtxprGonl3kAvdOXKw9Y+9gRaK1l6sC/M",
- "VIf+L0ZGwp5TUrbEB31pSkXqs0+gO2QAD0hoMmUhPwaTLUpVuQ6iCFEo1jfs3mALSS7ywmCuLkSAfC73",
- "6jXwHqn3jFcAjNDm/zyxq0Rk5QwI8BVlv2UPNxOIEeEWpD4lcWfGdE3Q+2VHi42Ki2JaZnMrwPBdr0N1",
- "3/wI4vPvf/sPsFgQTLJsZYyO9UoTm1KvKcvSvSLYI+D5pyTGqvGYzGiOOdIZxNEgWQoSKl5oX+C+rmU9",
- "eu+xaT0JPesjsb5pPYSyKrm4K97PWifq5+TQ+kANXHqKsf45W9qXr8Osl4ymriP+6pQe6IG8xD7durV7",
- "/4C8df2+fctsb7+7u9MpDE53Lvtx/1CHVF1pw22PF/DETwz8kG8k0+TDx2view1W2zN5raFkQ5/mQjSz",
- "ZrhhkXAxYDiDK40LxwZSAyu9qS5urpsY8KJoYMBn0BIa2q1/YWt5I/vjtNIvzfRPoGlc0dUD4llzd3V+",
- "iZnalfk3pZedLXUi7X57oIkrh9vrEcMU1jHqaoghErW+oL1qv01d1mhnrolbbX2DSFwG7fc14bMZSzk1",
- "LFscIQpUxZDwC8LkIXs85QgUclTbfb8svG1CXzT4p/vJKAogP1IMyJnoYwvNSkrGyPeyXm696g8k5JuP",
- "Kc9wWadKXRU5U3OurY4r0kh4TE7FHGiz758Scvu7ccLvQ7Yv5hf4khlME9trsQLsFFyX2c4z+tHdSAET",
- "q+GMXXoBFZ750jG0pwl8A0pZ0xVaxuRCm0kKNcLGZy/D+sGTIWRf5iu3XnkdNPb7e+hpLhOrnXq2oojU",
- "+xA/oySuD9QUZcGD6n07Xyk44gjufVZOeuxK/4BI2Uh2q5Hf6GdO01hB1Gy6/DRTX9sysQoU5khu1u4a",
- "L6SqXwbQZEnMBYcEep/6iOYwAL1A1pAsTF+O+yNroGK2j2B3CBvpsGMnLCVxE/apSyYFzFoOGItQmF9P",
- "ueRmKdmySUSfAMALwIA+j/pVDrCTg+bVk7Jgo2HsEJi+oLJ18K+b37BKYsaxROLR2tmZmHNsJuw560Ey",
- "ZP8TTz8jz2esqQnBCdUJtQqfq6yzb73QJRSsZVSf/OMx07FPL3ywDaG+iWHfwBuBYTd59fDxL7vL321+",
- "44M0b2Uh0qX9wtk6VKktRNHGsChfHxRtCHjmVp9sMl8rezaTBsohfePVlv4CrtpPL7RhjQlYZUeEZxI+",
- "qy0XvrDl1yZ8nMH322XLp3BC4zUETRNKZknhZnuEHMI4T78KW/4s56Dxmr+Cyx3EKcyrn0ylZoIYNsul",
- "ompRFjpQzL/1Lh840UZGonY7g18GvfTd1ot+r+eqRbHIG/r+gFwfQxnisVi4AzejgNPDDCSa2RF7hIsk",
- "K1Ls14B1NF60Wo3DUDVhxsvrkJBWCm7FfJFOa5jH8vhFCcr+bJGe6ji/sePsp4XU/6c+1ZfIX55lArvv",
- "cKBdjdhay+Q453+wz6xccku1uTTzaLZwHCXU2PiQE3rAIX196SoKYD4MWilIhajEe+FVDBBlGSzSxUkB",
- "jqdz6Hr2BtkC9W2dplwjmmWtaUXPlV0MdNtkcv2BLb62xTVblOngGG3N8B98jHtZ4yLPMu0GWDXI8/Jl",
- "nlEuDLs3L1+SeFxk2fCWLWIMk2euDN3xRKUIp15xoafyToeSKUoSmS/IqDDGZ915qKFKHQ1COZKFLNAw",
- "04xVIFGiji/iG5CrstoTy0fwdde3BGqmcsXG/D5uN9tws5/VcMMhvpLphoMHQ62Zj5PH2nGPNrK0LryN",
- "5Vi6mXUbZOBGy+raY8i6cKC9in0E6E44g+pY+KqH8hkqFpG4ZQtrbs3lrSsczZmaUbu4EOhR8s6lnbvz",
- "gEWiM6puWRoJLBd0KgB0LvI5oUXKDQKZw4ftzafmLO0h5kGlmNkVF0N1rgNHqbjYEe6w9E9/d/CqWdOw",
- "MwgM/xwa32ZjEifxj2JMXnpG2J4rmyqeN4bV409RRzCW6mF4NeocQkunz3GZblErQXZJFysyF+Pd4D9j",
- "93lGBTVSLYhOFGOilm5BulGH6lvUh0OgAszTPJNYRU6aypdfVmrwRIoASFSZqLMH+Jq0hjcQyslbIug/",
- "+hU/v+t6aah113t41HmOa5AXncM//1Jlk2rflHIjYEPRedhXhSBha0kXUfRr13Nhpg2chLZM3VJrvLv/",
- "yBQfQy2pC8+VPtMeQdRp8DzEgt1Vf/L95Rp9pLEP6tlT4HVBtIB8Nx/XpysS6G4xJU5DQpVaOHOLLK0j",
- "QGxxDV1QI0EzPmd7AxIi61C/U+o3KGulZlV7qgZ60HjHw7DPbFnVB3lsDl2wg4rH+jeeyPlQCfkuWyyb",
- "+Rfs8nau/Rj6+/TQ6R9fMdM/AQY6JBUIkB8wYMpTjJUeBbyQo0hc0Rm74ob9cGUUT8wRuaBm+sN+XK/7",
- "AP7M6SKTNHW5RW1cj+4VgCSqN9arpLahV4J6X0TJ2U7OukoNKvyBwZKPxgw3ie0xnoM34dtfydJ3Y7fL",
- "2HPfz6vT6yACAMyhZIEm55FrIoYypuvZoEeWuGCvs05V+fylD1XLxXF671xZDhyn9AWMJWQALS1363sj",
- "kxNZrEv+AF1ZV7Kb+5qnLAxoVVor+rnAnjT45AiRf9AHB4lUNZgeAFdrPcFH5D297x9P2A8HccsxsFPe",
- "RkZ6LoDcrAcKyJqoO3Ulm17OuTlvpvNsu7pwED7UGMzVdBEeXyDmFlPrSnoGLkd7nTdLqJVMR4SODEAJ",
- "MhIAPTouFPxB0DmfoDo2YlMOpnez5GrR0t6zZ02/ZWsLnyq3z1Pstv9etUMsdo/dvOHerbtx21FZaujq",
- "6NEv0DXWszYv06YPeiLCqkQihtYTmjFh7cUeqfyb504rq/ytoIEjfFfrSOhcGlKIMZ3xjFOFLnKN5Q8x",
- "10PH6+62s8aqFwcwTUQnWSyDoramaC88tNaz5p7gGJt8c14+PMI/V2OY49pJDfglVa1oe85p8Fc0hWcD",
- "Qb+aqf4UUvZx5rcVywAbOyazRUl+QD8HjMZQipSyOU/Y+otxwk0/II42X4tnQjMF4akSoFXeEdcc5geH",
- "krvXIxSxaO3p8A36dSSUvMOz6RpCQv46AADAEzEkGE+gOwwgAvjSoGRKuQAQMEkCJL57xT5XaYMJxxdR",
- "NF2Nt2ubM2cIDhsvA0thjgugCEMyDZQRljeRkJEo+/iA1ptxcRsgu0NJbeWhObe6sx+o/AGDi+XAfExS",
- "pp3tH4kYYOOh1YETKCAXraqTKz4HqGxLyCMS+1aEM5myOBJppf1zjI0g40AKxLPyMCfUckxfT6WJRFzp",
- "bwhYbPUOh0FoBt8HOv8QVgYzKnxPQi58s0tXEEO6MS2MjKEYAxoBWZI5fMlZYy7ncZr+xA2gcz6Ptl8O",
- "8JW8zW70Ne7m0I7J9Qn7xiFDhxaqD5czXyAzKOShrn8r4LJq35t+pbjMrvkbB4ZNPWsB93Nw1yEsn6Ei",
- "pfBs2cTJC7gAEbss5KbFqA8nbbOSMmOGptRQ4FvUWKArSuo6PFhpYG+YHgGgPd0rWxXrQSQufIjIY/tR",
- "xciH0z+eXlaAdh2gvYfoOyoB0+y3IhHiTIDs6atJ+SpcXQ02r7bONqXkJ3joGmnxjGpJZZxNqgk89LjA",
- "4dOwIEQQ3WY79rs4vtakG3hiOQ5dZ632MCJmkMMlGrYW2SmEC9H5N5LpogYkxESiFjkCBaH3+fj0qv/T",
- "yXuwLAPgIkpvTInz2EKOowAUlOdTpuywLVdEbYUhilPlw0j4ylUu6nHsKQDrkit7HDzwEWCIrHRFjoQ1",
- "57gmKRszhWeKUKiHUADRTjU7IheXr3AXPHiS60KF5y0SHnwLYrpi0R7IrPDgs0YzK+N8vUsmrLT1hCFn",
- "/9e4Ta4MuBoheloeZdJ1x4mlfWo1X23Wnea2O2RjePXCx0OzBVFsJueOlcPogMJSi9aXGqOPO4FeD+qw",
- "YgBk5HtkOMGRljU4rulGULsHXqskb/+AEI8fP5A3p+en16fk6vQaWo8CCIVPzQI9XfsGNm4ExebSxatc",
- "QwBuT2kftZP9sv35OMMeHRS7LnuCQ2i2rBMqhOEZoX761jZgfa92t+fkLh/i50/N3TH36WkYNuTortw/",
- "66+bJzeD1zL9vkOM2A7OxZ44yDJ4ASkCWgqaecwJjDe48JmaUMG1g2L3b0LrLcbwwlqN5cJRoK55KVQj",
- "aMNyIsf4BZqmYNlCNmOjUZMqy5pg0NFg0kXCz89FK3Ke3CJwRUUTtfdjodm4yDAcAnmk+87idQ1QA3hk",
- "WCMCCCCO4dXx+/N+rqQD3pJq4lN1XA8XbFKxb3/Y/wR+qs84wF4A4LdEKm9oROLIGJ3XfNhHS05RNwgi",
- "U7sn8USOFoSnbWojnL9jv/mP1BuX25eXLLVVAw6UCG4yj+mZSsv1tPVMbQC8QbwHOfacrEn3FfoYvyEH",
- "g8EH2My9Lyd/3DX7vGWAwRpz4EiBbSqIVs87gxNAaBLSYFMjJyCfWPkvVdVyd+dl6+0gnX9rYjn48LZA",
- "qsBGQjUZ2yNSpYAaPFr4fkkJqC+RyAtt72eAUyENaCp1QWskyWVeWI0eLQ/4CRFWyr4CsSMuOL08MEHN",
- "tnW2Lh2PecZRF+pHomwbBr3OSRdqokvhuweSudLSqrLOSGjG7IXheqdDw6KRhPvArt/dQti7wrXRHpCf",
- "obVIbb7awco6A23KjSaxrySoSuoYERhd2Y2/V6QicYNYx1anVNhp9Aj2PAHFTJbkGgJilMdiDPBNHvHf",
- "GmQN99GMg6DHT7s7yCxyntAMxmy4ip75jiE3uWWU7w8OHDti/orzjnS/JzmdgB48Jq8ODvYG5Jwq6AxV",
- "4QaipyAQFMM2J4jMjBFbAyizY54ZBsUNUgEHEkpmAEju3bce/mrdnQeNtzbli3/MEZQRUsj6XGgmXJdR",
- "XYzwDBOcDtROFhk2HR60pH7/ZW2gvtc6umcxrPwwEi1mBymMJRhGIk9bLu6VjI2cRaBkn2ZakpHdW9Oe",
- "ne7e222il969fbciBDQzR4RPBLaaMlOm7rgHP1ozPsy7MUneBaCkmjxHrnxdgwnSdwf1pa1d2La6i3J8",
- "+RDFJWiydv7/R0/5B9RTlp3jnP1m9ZR95ybQ+4hItUZLyaHdKzgesRGTw7BybQ8x6pSyjMMFf3N5jjfH",
- "qOCZscqBQ8mC+nVoL8WhnSZD0OJDQhFZfUYFnVjeKIRgWa+e69v3zSwubn48PzsZ3lyeky4fsEH1zuca",
- "e/i6aY4WkeBirCgmBvme6srelhriFPeLHuECmsb0IHuWJ+TsYg/0DiEFtmo5XpqZHebjxfXZxw/H54co",
- "M5cmhoKz52mjMWxZtroSC/epZSO61vaDziVPMdVeoDMo6gjp3ow6GHfJlRxlbFYmXru9wR7Ic56idghk",
- "aMmX+Rln+RHZ4BljD/WB1uKCrXCVY9KnSKAKg9S52epc7vwuj145q0j9Ri9keaIUS6RIeMbaw/eXrO/a",
- "QKFaXAsj/4Dh2tKz8UIvT40ah03qe9g7lqeaETgboWGBY1r7R2ASx1A9KIaLRJ119wakxAcdkMtC6NXW",
- "dQAyQyHhIRKVz7uOBUf1KFrZQIAbggHx5gzoS081xyf6C/BiGPMS8hSakYTcI0QWxoqtrxwNu2R9H/eH",
- "MroVxvHKeyXMs8QlN5fnG1laySJvN12PEZHTmm7Iv/D8EbjjJkVGFRpXACLvw134DFwki0iUzZm7yxmC",
- "LzSJOoCcYn+GtwDtEOBArWnsTdm91mgqzv4546h2hE0RVHjoyVK7QMOw9nV/4pcXNAj8QzXQ2Rh4s489",
- "b8jNjvC1gm2wutZtSP4LIMDgJhBaYZO2Uu3AMiuHfif8F3TI6CnPXZAbUz3KTCsPgeSDZaFRMQy2Jr4U",
- "ePWfFPRlhy3qtUKptVDp4Asdqn8Qmv8EGAY7EXytX+mP5ZfO3gRv0ZNh8TTB5jyn6K6M8JXqb1q5zEOl",
- "Tn7b3PY1ZD2S5ilk/b4T4mshOmCL3rsHn5sVcJxN6hU+9WWhLR4niBALA4kIocrfvlBqVCeP07SyT8+Y",
- "IlwO8thiVccsFBo2d3kwcvf+qfF7jtPU48uB//HJZMX+J/vRs/XlIZeQY7XMKVvuFCZofaW9WrK47Uw8",
- "HQGo/Td+cFdCPIC6evYGuwnb1bSMg5v6YNfyr3K0/hb5vX2g2b+9FEnSHmJhNYbkYrB2lgib3el17Iai",
- "GdDrYJ+upvhSr3mslWjVlu9Bl5Xai676o3P46uCg15nRez6zc/4e/sUF/utVbzWK9JwoUb+Xo01X6e/l",
- "6DeT6V2vPNK+pInsE+j4ggHb6kErczTrYiuUHazjyAv/0DNugBtj0yZchAqpR23EFg32zlx+uo+WNSIk",
- "13toeyI11HSsdzpdhHKQ53M7uTG+kuPJr3BzDdFj76/nDZFeV6sHfNLClGoSZzKh2dBt+dBjeyIudSS6",
- "CRVC+kUSeDiwzN6AOGcxVYywezbLIX+htJmed1HHofDQgVBxTeKp1GZob7449P2ClGv9mPz4R567y+DU",
- "971+t6qi8n/d/zSlevp5H+A++trIfDusVPvW06ClvqMq7dNRCBYntcqxnOcs44L5fCp2j6SIRBdDW5i/",
- "nu75lQ/Id69fl3FNv4tcOwaD8k/7f6HntXZDzTmFV07Oz8AnCR3DhKyhR4TpGBkJSy3SLVz61sn52QtI",
- "RyMJFQnL9k+MyvonLvnqTrpeQbpHRtJMyYhp02fjsVTmMBKEvBqQC1RQ9n1Xj1ph7TcrRbPa1WJybd8n",
- "BJPB7HFBxbrSHAibnriASVgDnr/wsoF4KZTeztjA/vm1ayWIcU8uQn9MJJgv9+9i5c4eFPrC2jOW9tx3",
- "76Y8mZJCjLB3o28bDrga+J39EZtwgUW844xjqMe97bfP3eVlpzXXsSWDX1zAHY9kP5Ez3315Wohbva8X",
- "s5HMXO3ex2uipJ0gfqw7w9Yo6FzGPcQ1EM1mVBieuDxESipwdXohkv3Q0lzOmbpT3MGNNKJYv7Xn68rI",
- "/My+8pxdTsJI63SGt+G4+/4RX7LG4muWElVWToXl6WqznGUp82Bx6ivxNybxemnnA/S+cm9Avjv4rl2M",
- "RaJrb1ghy+J8ouTdHh7YehE4VsYD2krqZEAFDA8Co1QbFurhXcrAlW879ve//QfxsfWWXBCnr1Rrv5+v",
- "EBVz7VZ5ukaJf7SCIegmB/GtsIpa6fLWTNl7rsu7GfIfW/S7rGA3jRca5KNdwFSmJOWKAaBuuI48N+d0",
- "wg6t6O6HRBbEaXYsmBd6GrKhfEKI7zA1sBwKV18tleEFLYx8gdi7HkfUAGKjLBMg7CSw2I2QLqIYLuVi",
- "7fv0Mpeogli+F8fXDr+GIJjqod2qof3U3oCcjZ3xg4cD6uR0r5ppVusXaj+SS2yyD01f6ELb88kFiYU0",
- "LB4AYdwjcdmrdwrdZ+1upUVmv8pwCwByGIuI51ZEENKFt4f+T1YgSJHquEekyzLeQzIu0dCr6i9wCg6P",
- "AovlZZnZA9t8FFp+SUFSe2embVsTPtsLC5fjMVzeyEdAijvqWKXkiYqyRNIykUVDThzITey8iVTOFZtz",
- "Wegs4P5ukKbtbSfqgu1qIZLnDaaV43wt8PrVebRlOPkgG5x1398Uxvwny0I+E1AGDwsdolB7SsEfWsOS",
- "KWDSrQh6jyFqJYNGFtxNG0Hw+VW7rtnpAK2L7Ystzt4H3R5FIzZmtiDnH0+Oz0swom5NpckZU3ugokDn",
- "Qao1nwiWYl1QsATDy1bBh7Vm1kgZLQCbZyIqHaatcdicRIjfdjT46ND6n6dNBQ4FY3ylU77G9eRPtVcy",
- "/rnbVOBWYAY0AKhDdLXmimqL02x39Nzd+KWdKqclPNdYMT2t+xJ+lSOP49SG8rXJjWLPKdouwZ3Q5Fnx",
- "qkHFt7I3IG9YWuQMa/ZyDVleOdzTkSizfoUDkQz9Zkpb7Vc5AqXhg1QzyC4u/UZ2aSlLoLEsF4liMyYM",
- "zchcQ6FWPSs5Et3qM7DagE7G0qGeUgfklUhlrScrSIxibPCGj8eRAMgyluoj/LZvpNeH93skp8pwmvWt",
- "HlhATVwi50wtepGQaqVvPaZB7w3IBdUa+1G4Dn5GIhqv3cwiyyLhqbpcuI5/TRUfO6ghnUPdDFqFPrPa",
- "IZtqEpcIE7UV2ztmqqRAFcp5jQRU4QGRv3GuOYpdJ5XRPrppPyyIztAl0yBl4c3Stb/2Pvq5hCb2E65N",
- "03u/0O+FBf6RcDBb0KConDkpfWdEFQLRzJCcJLhxuor1YVd8dv2YZ6xH7njONMkVt9ZyzaO0r9hY70Nt",
- "Ihvaw8v0nqvnlH63kTRhK3B32ivh7IyaQ4NjmmkWQoAjKS2tG0OAT9kxF0jjpEm6zqPkHg0wZ65fC3rJ",
- "Qwvjf/9PksLh3/uv4W167woXiGJ9b7+2O7l3vl0wSXabQOUVPvncGVhn6XbJ7TzVy/eKM2uN/EdKyoLm",
- "JGVufZv2Htb22KSPZ9XRWwDsswU5/dP16eWHmp7u+h0t6+ozuoBqY1ywPe/2fyF7mwbUmP26guXbabXo",
- "5sC6buHX8jkzWmEkN8RjE8eukAL/ZVLGYL2N/P+IDLJmebf/aYKyZm0W2Y3QFcZ5q+Rs+9oA9+5vI4sM",
- "G/R4cv79b/+JZMRKp9+qPOk9JF3NbeuD88iW2cX5D/tcjOVWcCpY6JYt+lDsDV2IfGDm5vIc1f8pI+/e",
- "H58QDK5AS+0a27fGpUGSBgGK8jMSpRIegwhFqyHhOTXQfW6lpDWYWX1LvIqt5ePVqFtkfMySRZIxmLWQ",
- "/kMBC2RKRZqBO91J34PvAAvyTpIULK4EeyjpHjShARus4BqoAjgmAB7MFTskXbrnmtFRMwVFOCYeN0sx",
- "LbM51rGLsP5IUMgWwvLu7mivpg1gUgXA1AUXLTmBFBMEposEINMZmCqdjfiksOQCwAtQqEkMWDJLDBE7",
- "aDHo7iDFmKsZjsVEgg1QrMHBqKkHe6tsBGSiOhJRp3aJ9Sokhvbq3qkXddbHzFxk7cyy6PMXrtph1mln",
- "7jGSSKlSLqh5PKTE8/pmT3noS+W5B2w7IR0b9cjHyxbmikTNnVE9iYBzXt9RBwC3N4jEmyrTjRYkmTKE",
- "lVvHdS576Wnsip8rUukbJ4rAIMZQjQ+gaWbgxvticcJGYWw/urYy8Di0ZR2QN0rmddsAoAW50cRZ3T1i",
- "ze4eWOcEre5eJACU3rtU9IC8YQjfwOeMMCGLyRSBJ6wiwpQHWao2n0c4W+jFBYKkBP3gpr3gsJqnuGXJ",
- "IXDbSKaL37RG+OjMtFCy6DcSYqhZBnvpPDkEAtybPaztpYyt9D/4gomaXzLb4JG78hMzpAK3jmD/cMy3",
- "ERJN45aPeEq9sx9ckzRQPe8hzcTBQCCI3VRxAUFdZBTw5qH8jUSX3UMyyzCnxq5T98iM3g/BCaf5X9ne",
- "kTvklXM8YoQimk4kNM8QyzdlfQ9P75W0TYHgZ43+PiQd+f/EhJ7G2ffIU3VhGb3MVvQ8/cDQEVyY+5Xk",
- "yIbw0a4ncW0/bEpiVWDfEm/jzGhO5NhDgGSLvsOgcrzmLt5IdGP8wfm/4z3vdkcIPzjOdooFXAUpywyt",
- "hjgOXTK4keBnryV8Mo/a62XAgNhTByiTLn2l6cBChuWP7PlaDpYDVI7qcx7N6oCbe0LJnIl/9poBPBsu",
- "coPAW2UTA9fkxiXUhYvOKfzwunHAoQ1VBmWa2liqGeCcPqa0+5nLFETlgnT7z3WIu7jM7RApqwrwLyYo",
- "t7R5gMmZuhF0TnnW4GH8mDOX4FZfcEWw+p+2EayY5f1ckhUm63weq134ysat8aeoE3LmKx2x+ZjQSPgt",
- "vaOa3HJIqycxBALhCWEVOfsb7jOGeU/Oz+AcaFcawAX25+hDcLbIiRSEUZVB7YqBvgETigF2AymEcGXd",
- "Ae4hYMNGQhWCYPq+1dEA/lSqoGRhJwB7YF71p7JQ5Pr6vFUunyDVn1tY4jBrWy4i0X0Db0xy+4fR4nH2",
- "yF2+OmNJDNRc1w87Ilal1s91Qq6YSK3mMQLVSY7Rnnf9izVBoC8EufVo/yKoKYNIvMcqWfL9AbwJPQmA",
- "8cHD+fLllVGMzuwHBJtIg0DDL18eEs1ESmLs4XNIqox23xepZbYY7ATFEsbnru1IxgXrpwxKd1lKNHzc",
- "zjo+cykNAAd5OoeGkwjbarUj6Pw1BxA1wJwQrOd6M5N4yqgyI0ZN7PINXh0QvTcgPzvAR/R0Yt9gCKeD",
- "z7hx5jDrvSbQ7UhkbEKThesj2P/91ccPbtJvLdn8GYlLyGY69jnSsDeR8FXSuvVYw6c2JXTEzbTWIYUc",
- "oU4tZVka1uGI2EhnT1PIKYI8iUMSr9Clkm2BxCw9XEjLxnLxFQnU6zTNvxWP+Jn0TrdpX8VEXOUaEEuN",
- "ZLGUvKeWb2AauK3wf0GIfXgDzOjOUsNRsXIL+q51Djufog78GHUOow7a+ob+/+xd3W4bOZZ+FUI3kTAq",
- "SfbE6R0HuXBsdzpYd+KxndnBTjUsSkVJbFeR2iJlWWgE2KsB9nYxwDzBPkA/w9z3Q/STLHjOIatKLslO",
- "LNlJo6+6I1cVWaxzyPP7fbl1h2Y7buC2AH/Lox34CbIj7oeMS9UZa/gRbsRyn8b+TjtugIRD2CBu7O/2",
- "Psbq9kBQ9EMD1T4Vq4LcE3drH4BxyXs+oR034PrLzP1773n9nBKtxGdNKGw6cKE18ONub/dF1Hse7X5z",
- "sfPN/u7efq/3n3Fj+VZcqzAy7LqXHDQIbJfdXhj6kgrv48b+H59/Ey4OvWaXAHTt/tpz74en2/1lsLIN",
- "rEkLBPZUFDSUPNakmqoWQwrysJejQMYKXtmwZih/JF9WA9mTVEgDvvYEaQW34UvNnPjUr/OHRgCb8P6M",
- "oR6VfusG/ymTBmpFn8h52HaJPzgfzPub4FG+Of3AjEzEkOdsMDMLwtp3/9tm/TNh80V04M7KfjiliVCC",
- "imPMbDwWxsnMnEvLmtR+Q/ijeAvsjqVnVV/mFuDHx6Wqi9kgk3bZijKsmfEbttf7fMNPSTPZnOVXazHA",
- "EFs9Kd0IT3tU4gzujtmEnuWvd8+YqSul5+rL2TEeGG44hE+ylIN4UMSBsIlWFaLA5sIrYRxw7fZDiUYm",
- "k8j54lM6/jyX/nTCjei3WR9P2UQaKD0WSTccuF04cN011QO6345VX0AZflJqDwT6fe9r4bYHSAjLU4tV",
- "paURI8cF41DALJgpn1THd4HWP+CV6S9ZBjRRnMHSXKHFshTTixU1QU+kQU5hKCrZh6gKrjYYLjJJRdz4",
- "2F/pvpx71Kjt7gfebLkDzQe/LXnC4Pi5F2g9CUH9CSDe+znpUQUXPJ8pOChTboCaAaG03M/1GvKwdOIa",
- "/TKC58PJtiIVx9gTQEAaTswUvCSU+/LpNNc3MuNWMCV4LoyNlJDjyUDPcoYTC2wWS13Sw0muM5FFYw29",
- "MGLoBuwwbLmEmHSs3JQiBK9CToZ+JtWlGeocNN69v+k7U1VakUK9yzQXI3kTvT+LAl1RrGAjbrVZn5Kn",
- "7p5ByodXeI/hWdEK1CL9T7kaz/jYXfvrf/8DEDIUy0Q+BiPYauenRRC1CdXPCcu585XcRAfCWHwmg+ki",
- "Q34x+wJgAwBQokAd9uvf/9c3TJOlzvq9zm6fNbH9JxepuOZqKNgo1RDa5oRiEsgaQ4o311PG3Spwd2xx",
- "O8t5GvkXg88pBeGnzCfaCJw17js4bWfv/63X2d1rs17nj3s/tHCy4sZtBdJNrQ8zpgZDiORY7GQe6GvB",
- "vnt3/h840aUbgaXBqZe7G2pT8HUAUabf6zz/A/a4uE84pBcc6kREWAdDsgUZ81QOcgguu+sPdSLOuLoC",
- "sY3+/G8tWHeQ3EsrM3GZGexqcuqOVXQ70DOV8ZRNUz6s7d05p491jqq2pQLsyiBPZLotT2LNXl2Rfygn",
- "wlspoGy+/CaXL9YjOw61XCWn7FoMLWiE08tMGufdwwlUdtNi1Sz5U4w8MyPsnX7Xsm0O1pDTD3DfQjSA",
- "wjng7bkB6zAgV3tsXkSa+DIt0uPSiUk/rLUo8ZpuIpyrBhR62/LWUA2OSgNtR/eLEZ5I78sTWIMe7ftl",
- "y0v/G1TzatGYjqyOijd2xzudQhBI/yzZ3XB2qU5qfVpiG/Lqnv2k51R5AveQV0qv2clvX1zdykB3RImw",
- "8yG7bGDsN9suxAL6LaLpN0R710dN6zPkEgeadK6YTISyciQBo/tKqE6s+iRXfYT8cv8LVVLpgolsatFx",
- "6QuVXELD/qtX2L4N/yIbnziSYMWUnE6FNQxmMSd+VpBu3zoNMpWLCIptpiKPFRo+LyliHnhdRzpN9ZzN",
- "phgaDXYSLjDCDmK9DnZTB9CoelMUhT58lG0BddAAT6TfpfHXtV2HVfjtazXAuPj3pXQx6MbnqTX1E2z3",
- "CDqnQbbkMMHTn9ZdqkzhHgeRX/bfuryel910ZzE5U4k1MTzTDSdT61OF1w/w0109Eed05faLxv1IdRkO",
- "/6evprbKJzn0tciRl97qqTuQoPNoiPCv1IkEwWrT2kb3xBoRKCHX39XVioVFBa0axJkm3FTqQRm3lg8n",
- "kPacK2CNj1Uq1ZWHjikDNlbZZs1Ez1ncKBrW4gYbTuSUAHWB+gJKwFOJGYIfZ9nUZwqKaSXCcpnC8yFM",
- "eAzmCjBc12AQqWcW2mQBrUmVX28hLNoxAgsAnNvOS91GHoryWvKiTy8w7AIf/kBAwBRfH4ILvmepTFoH",
- "zbYDIRRM3a3dKlJJ3+dZfLPt62MY7EQau5oDCV7lK9JMgNwohB+F2a0bB1rpUoHzdvXxjkSe1z2KYXsc",
- "1312LXIjtWoX3cGlnkUGMFBtJ+9OdFF/0pRnPKIH+SAXQB75/vRmH+67TDVPRNJvtZmaARGOHjlr/BYX",
- "A8b2wzWlBg8P0hOynT/qwSrA3+0nzHCEtblzxASnPNkmqFLPcZ27YaUJdrxZqeBe2u0Honpee17cu6QD",
- "yt2UzRdQ7oa5c+LezllzmOpZMkp5LtpMjXPAl71w3hGV+4YrhzwHvngAq8X5vmTaSRA2NeTAwy4SaFqf",
- "Fdlu1mVxY6gzBMrSqr5P3WncBb3QFj82DnHILU/1eEVW1L8uXbMhYlyC9/XLaXz/k/Qf/y7GY/qxO5CK",
- "uw9xJyE+6TeDzmI/7jPD+NhZFvCYRfn7J14AmDSxIhpupmnDw62hDeeTMw66YY8h3vzQHk+byJCrWEll",
- "LE/T7gzJAGWp6eXDW9b0xPVuZ0EMHwGNou7vR3p45XYnmfExwJ1RcYBl9FCCyXO/0HPaVZ5msBlMrKhV",
- "nW6D/0LDtlaGNbEtH5Ks0D23Xjhf+8XfuozCSItVx+qpyKOgmfQpSYw2ILAHTkKiymNh7/Ajfqqodn/y",
- "d37s0ldYzTx/pOcKK/PhXOJWGOjSc1tJRXQDcKOXIjS6dL7oxAowUIo9CGw7ug8vz9yTgAYeUj6xah6+",
- "/evlxYd3745PLl+/fXf5/cG7gzfHR9AQ1qKdbi6NKOGY/Km+zgNesPwVG/fBQCgt7mocBF8zXmite4DT",
- "2pXl4U8opW+9or4Mi4Y4ifONyOwjtb69rhObJMBlBHNzu7PwauEViBGL3BIUwvJVfPXOX958Ww9Radzc",
- "V2v0mYiSeyq1c3ynKR9CO06h2rEa6ukCClasc8fcnzxe/siKfM5zzJ/mMxVEjA4oxEOK1dJmsEbbVzfn",
- "/67UoUf/d5XenEqTdVSr0YFGeZUe0yFIOvV5Wo1u4v2Qy5xWQak5KSDey5p4kHbduN2JNtZpgPclhlop",
- "rKiADJJbbQUxkNAK5oy/Pvzx0gjbJ2+iMGK1EtBUjRgAK5xFsu7xZbbvReA4dS2fVLZIy/q0nJVvhK2a",
- "xZEXkcoH9IwKdRKzAlb/FEWB2NRQDJxFLrNMJJJb4Uwwt8jCMGn30dQCNxCxnCEJh6O0Sfvwr3rqbmgj",
- "UouPWASh6qIM5YIeA6BJ7zNpSYAAx/pKiGkV1Vsr8RJbMrmiLCVlbK1G3Oe79v2SYG0+wVIeAgd97PwK",
- "zoACLnUQAPAplzZ+TMPSRw37fu/xuDI2wweyGVWjvbpOrxCTajpNF0za+27LJOJly2oZ5xwuwC/XeELZ",
- "oInUmQVfgz3wTvvudlz6RzMBvEFbe/SXrd3K9JiZDSiJeF9RuhVJXnF4bj/eeuc2sxR39KFRXTTNYfHw",
- "RKeJyFsbCXhUVxdHNIpPzUTfW1ud/bXaCXprDNJkvDm+8DYb3vnMEBwsgSz2uxPBUzvpv6QdFg6bWAlo",
- "jcB6BEpT4QqJZIzgq7meWeF7ZCY5YRH6cWKMl4RYHpFNumMFS7Qjm8tpGwJqP86MJWIyJYxB4Mm68/FC",
- "mEfbftxYq1mi3F/pOGJNffUK0EvQ15upkMZofW0bUUVOj91H0pEzYsikltfSLhiY/re/+F2S6xnvumNp",
- "J7MBwYjel7/pGQaCAY+LNXdesIm4cSZbblpbB4s/RYXxTCMoyjM7gUrpxZQb4zPK/b9G380G0bkcQ1OG",
- "iHb3XhRttIDbN0Cg5ej8u4PdvRe+94j0DvAz2ZVYYK8J2KxFY02J2aTKhdnvsO+pK1EkzPjRTaw8P1Rv",
- "56WzRH03Yx/RjEsgyR32XjHO0MzpT2dm0kcQaPjAOR9C+0vO1XBSjruLgpFnmYsnVs1kmRFnMMuN9aDP",
- "UhikESZY1/4UCAGLv/rmk91eDwvslIYcluccZkZjPhEAYRlhHxNLYarnmFStp2wBEJQ3IImEOnsXzkfl",
- "q117KBGdLNpOFiOhhjoRCVUCTvju3otX1LTUWYXTUSMtjTvgx1c8h9C5ETngDiH/XIeCJ4nEysvT3C2n",
- "hbwQahUNgwgxj+1L0Ac8IMyG2hQGOGU5UzrS04A67nbaTRLL3GMiRx7w3CNMsGbglilRy0gnwXI8qeDz",
- "b/c0QPh3L4vVNufH6ML+oEK2GApiYP9+qLckhrNc2kVj/28/LNETEggS7T238OibaCW1cbNelymvwbx8",
- "QCWTxJCyWRgrsrbzadyxgFDbDDP50VwmIvZcDtfSyIFM3cHsqVQJL9AIYcp1JdRv7A4WrhaI2LYi//g4",
- "ZT2Vep61+O1heVKIEz+p3w0Jdp6mpaUtCUTpRwhl1XrSh/AlwkhbCvIsjfJJhbQ7m//I6z8sCedDDeb1",
- "Nx1qNUrlw5AiNyFC+GUQgq0Qo1VSVLuxdH+SyVr0+TOR6WtisC62F+BADP+8DLWCpSJAZzRiNTrmxWRp",
- "E6HaG/fkBKoN379jR8cnxxfH7PDg/PDg6PglVUiqROTpwj2hKNGqsipRzZZWUSLNFfJ7mFi5EaAcJHdj",
- "NPH1mIUuZt9kvFzqSBWksQIHLBHGiXZrNbp9VfPuiW//tXGvB6D6OwVsNQz9moXqPfIO8bUt/xthC6Cu",
- "e3yC9aSRQQHfHrHmh5O3R1Eqr4TPKYTE1sBzg4cbVnnHMvlsqvy6nMW2z7KlUZ6oKWStpHo4+fnjS+xX",
- "dfhR3qI4U3wl8aeff+EAWMsVGWZ06q9+DBGhwe5v2nq/50Em7hNtdmATB2MiFF8zbDfAnrZN7oGb29Tq",
- "w+fKiNwaxlmzsJVk0vaveOmGbTljCuoCY9W/bVL1qz0mEPvz3j2kicH6GTiPL1Z9zAK8ekYtHc/6HXY0",
- "Qxks4mDPe3+qPlRaI9IRlCrMlNUzCP85L7Dk9YE9BQ59MPFKsDym3gNUV4GZcts7e2mwp/ZQaBpFF0md",
- "wp6ASP++t6/YB9QV0gZSHUSQ1JAOeoC7s0xBts79KQi37lLhOpVqx0q6jQw4B7VizkNpVxieSPLQh1lu",
- "o3IqDSW8y4rI07TQ1NoKEOjQKrF7fpqr8oEavL628wOd1RKpFhXuV3zPZqDAC05h+G6tex8qDz822vdI",
- "UW0sJ7VGGwp4uNoI4zK+GzFBjrgyDHDY55q5tUlTzPFHBBuGiAe0rvssEcoI1iRQNzbURirRArE3U567",
- "v53/+URawb69ON9jr7/f3YsV5EcI5nBkTavDqIcAqXMnAkYnULUUyrqceoxmRiSxcr79mRhKt0XxlJ1x",
- "dcW+nSH8/9WrFz3MGh0Mc21MYXVwxX75ORqkAuC/hlwlMgGEeIA7a/Z/+Zn9659skO3uXSqdZ7H6A2vu",
- "RL/83HI/w1vC733M4Pzy86teZ6/NBtpOMCqeGpZJFWX8JlbuQp46pYFWBVjflkfAz0XKMas6yYWZ6DSJ",
- "VbNfTOjX//k/xGP71z9Zr/O83wI8t9KbQAMgEuEqHasAK0GUp6m4kW5d3CKnnLAnwmfusNNZLiJ4oViN",
- "uIrcxw4eorvunYf0I+QpZ2CMeZ6kCIYYKz4wOp1ZAZSpHFhEjS7vZbmeWalEuvD8ZUmsZE4IdpZhkIdb",
- "prQ0IkrFNVQoOclhRmYy5bm0C6w4QIEZQ0mqvPHtj4MFgXIA4pxlqeAGGd4oYWrnwHmG38VqoEJjmeBK",
- "qvFolrJRzsHA8de7BQ8ksQSEB424yCeg2GAmUxwXqhNyPZAK0EbyVPBrqcb7sXICG+3g5oSBezPLr+V1",
- "+aQjcieuFiDf0W6bCTvstGM15NMpCkzQBKPhnRKdSeUXzonuM8ssvxI4SKxMqm2HHaRzvqCWOGfkKQ3F",
- "F2OYMMuFe4OE/agHwPGZiIGeqXrUu7AfB9i7uk0SxKnYu/5r7caVSXUi1NhOGvs77ZWJy6VHWj0N9nIl",
- "a0kAiY39nV67kSElRmN/z/1DKvxHMUoBSrZmGPzk9YPslgfZ7d1jlOpO+y0AHGrFcj6/LeYddojiNhCp",
- "nuOhBhiYTuudQHiJGY+dGiJYJvE+uP0BW9UWWSZsLocEjlsRIoRi8KCSRmOmP6BrBr2NFQJ9FiTG4FbA",
- "PhqB6IG+ogb62BX8wd+JcDnQJZ4LN7hIiPasVw7OjnQeqxJSDw0RJjwXYkqKDozHqVbjyHKZAj2JM5Ka",
- "ojPusLhRSryFukYyWOCXuME4ngM8Vpm8EUmU6IwD1U+IgBVEGUuCEVA76+Wi13nebozcVm8b+41Rqrlt",
- "lCRlpyQnvSAn2IO85daJJQVej8UN0vHo0I2bMQy/WwxymcAh8Qe0REja/VdPUzptpPqsGMMGogjrTDRg",
- "9r9fdOocr92i1LxBIvq7QlJwGZPJcu5IGkY5Z6u/bIrfjYW03AJcB4p/U5aw6nJ88SGti8qnBJZAlvEF",
- "mZkQoIN3dK+8IJoqZjVUjSF3WsYXVHTgGeDghg77S1GBoFWKZQi+DZycf6jaqkgT9dfINIVKGGMiqPok",
- "gxpJ2GpR0twEwrpdaBDWbcFQubFoiE+KTdV45ucoKhX6st8g4ewmNA+WqqJqIJ6FHn5G5Ag34u5PY9wC",
- "l0JHyzEYUxGyb3OdFWJ2dwjGfF2felMRnGt9Vflqv/79H7ij4J7RxD1H57idtL6YLfOWOf+XIGirxyA5",
- "+nRDAavt7ywLW6J60Fdx42O/AI0qUC+Iy59CMM45kIrtxApJLwpGzr3eH4lor/rkmcIZLZBwTHDjjOr9",
- "uNHpdMKYWNNx9JpNAXKVy9R0GFVFkyfaPyib5X2PT+1XZ0UP5Xe4Glu0eXCE9QYyrKU0jFZi02DgnzKF",
- "8DnIAzp6vdRPsKao8cT3TwB8j69grMX1qT7lp8ZA8Fzk7hO6hzorAiWsTgPPeSYincuxVAAhpKNEWHAF",
- "S3ArZycgo6GCyEwFzGSWp439RhfwJWlWt+qsYQEwwEhoIG7aptLnPnDnzYo4LEvlSAwXw1Sw5uHZh6NW",
- "5U4MNty+GXEN2yUA7HYBy9kGTFgM9i+hvBYPp3/ffvTFJBciAjqbAoZqmmurhwDy6fctTyly+wkHp29Z",
- "ooezTCjrO2fprkQPa18HSW1Mm6V6LFU31WM9s2025cbMdZ5gu6toB9aTmSlXlLtTqG4ebuuOwMoDDLyi",
- "p710q7um5l7oScLOITwr4ECIzFBPRcLcG16JhUGuh5O33fOjf3djlJ47lZG7oubRxelERixV9YJnKK2G",
- "sLZ78FIcovolO7EqFdh64x6sWazYvkV4DBswUo1g2Q1ISKwyncjRogri12GnZzsM80NOKsFWfllMcUFw",
- "hW4x27Hy3TLtQF1v5zoylo+DCxz6UVLIQSmAhXZmqrKxykUquBGB3KYUvB0JrPDGbg7cmWmNSyfxunPR",
- "7OMhHpq7jbAwklsU02HHN4h2Vw7OJ7FaSoYF78n7Hm02zt0HAdLmkFEDDOVuoJwBZ6HD0EmFhXRvX0pu",
- "g/EY5PQltMB1yX+TJlZ4qZe7EYA0j2cpz3H2nugfrZapHF7RdyYMaVFZMHxuzWKRBJ6K3EAE7ADmzS70",
- "lVDGjeQbfOq+DMTPhqlWuFHIa26FD6qrhDX11CNgt5gHw3OXeqHpsHNALoiVUMN8MbUiibiNMOQvOTs4",
- "Po/eHH6PAfhpyqWy4gZC4T6cz8QNH9p0ESuthpACPX1/foEZiCqWgp2IXAAuSnVhoLUmgh75uvX5niSH",
- "YNioPRBPnUgDA4xF8hc9swOIcFPDJIQNx/JaGN/8A4cnL/c1zicyBQAw4wRpICZSJezdwUWHHQbYExra",
- "+bROJ5Wev0RIMkQixAJUTA6npX5N93hJ6BRwPsA603nopGlVR8GHsxNTWSLfJffxh4//HwAA//8=",
+ "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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
diff --git a/server/internal/indexer/indexer.go b/server/internal/indexer/indexer.go
index d641305..c8d016b 100644
--- a/server/internal/indexer/indexer.go
+++ b/server/internal/indexer/indexer.go
@@ -43,6 +43,12 @@ const sessionTTL = 10 * time.Minute
// cleanupDelay mirrors Python's 60s post-finish cleanup window.
const cleanupDelay = 60 * time.Second
+// wipeBatchSize bounds one full-reindex DELETE batch on symbols/refs. Sized so
+// each implicit transaction stays in the low-milliseconds (these are plain
+// b-tree deletes, much cheaper than FTS), keeping the SQLite writer available
+// for concurrent transactions during a big project wipe.
+const wipeBatchSize = 20000
+
// FilePayload matches api/app/schemas/indexing.py FilePayload.
type FilePayload struct {
Path string
@@ -341,29 +347,46 @@ func (s *Service) BeginIndexing(ctx context.Context, projectPath string, full bo
storedHashes := map[string]string{}
if full {
- // M1 — commit the DB wipe first; DeleteCollection is irreversible and
+ // M1 — run the DB wipe first; DeleteCollection is irreversible and
// must run last so a DB failure does not leave file_hashes pointing at
// already-deleted vectors (would skip re-indexing on next incremental).
- tx2, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return "", nil, fmt.Errorf("begin tx (full): %w", err)
+ //
+ // The wipe is BATCHED, not one transaction. SQLite has a single writer,
+ // and a monolithic wipe of a big project (vscode: ~445k refs + tens of
+ // thousands of trigram-FTS rows) held the write lock for minutes —
+ // starving every concurrent writer past busy_timeout (prod symptom:
+ // jobs-worker `claim failed: SQLITE_BUSY` on every poll tick until the
+ // wipe committed). Batches release the writer between transactions.
+ //
+ // Crash-safety without whole-wipe atomicity: file_hashes goes FIRST in
+ // its own statement. Once it's gone every file looks dirty, so a crash
+ // midway just means the restarted full run re-deletes the survivors
+ // (per-file DeleteByFileTx during reindex, or the next wipe attempt).
+ if _, err := s.db.ExecContext(ctx,
+ `DELETE FROM file_hashes WHERE project_path = ?`, projectPath,
+ ); err != nil {
+ return "", nil, fmt.Errorf("full wipe file_hashes: %w", err)
}
- defer tx2.Rollback() //nolint:errcheck
- for _, q := range []string{
- `DELETE FROM file_hashes WHERE project_path = ?`,
- `DELETE FROM symbols WHERE project_path = ?`,
- `DELETE FROM refs WHERE project_path = ?`,
- } {
- if _, err := tx2.ExecContext(ctx, q, projectPath); err != nil {
- return "", nil, fmt.Errorf("full wipe: %w", err)
+ for _, table := range []string{"symbols", "refs"} {
+ // The rowid subselect rides the (project_path, …) index; each
+ // DELETE statement is its own implicit transaction.
+ q := fmt.Sprintf(
+ `DELETE FROM %s WHERE rowid IN
+ (SELECT rowid FROM %s WHERE project_path = ? LIMIT %d)`,
+ table, table, wipeBatchSize)
+ for {
+ res, err := s.db.ExecContext(ctx, q, projectPath)
+ if err != nil {
+ return "", nil, fmt.Errorf("full wipe %s: %w", table, err)
+ }
+ if n, _ := res.RowsAffected(); n < wipeBatchSize {
+ break
+ }
}
}
- if err := chunksfts.DeleteByProjectTx(ctx, tx2, projectPath); err != nil {
+ if err := chunksfts.DeleteByProject(ctx, s.db, projectPath); err != nil {
return "", nil, fmt.Errorf("full wipe chunks_fts: %w", err)
}
- if err := tx2.Commit(); err != nil {
- return "", nil, fmt.Errorf("commit (full): %w", err)
- }
if s.vs != nil {
if err := s.vs.DeleteCollection(projectPath); err != nil {
// Not fatal: collection may not exist yet. Worst case: vectors
diff --git a/server/internal/jobs/jobs.go b/server/internal/jobs/jobs.go
index edac0c7..349b59f 100644
--- a/server/internal/jobs/jobs.go
+++ b/server/internal/jobs/jobs.go
@@ -181,6 +181,13 @@ func (s *Service) runPool(ctx context.Context) {
func (s *Service) workerLoop(ctx context.Context, workerID int) {
tick := time.NewTicker(s.pollEvery)
defer tick.Stop()
+ // busyStreak counts consecutive SQLITE_BUSY claim failures. BUSY here is
+ // expected contention, not a fault: some heavy writer (e.g. a big project
+ // wipe — batched now, but still minutes of elevated write traffic)
+ // outwaited our busy_timeout, and the claim simply retries next tick. Log
+ // the streak start as WARN, then a ~once-a-minute heartbeat — not an
+ // ERROR per tick (the old behaviour flooded the log for the entire wipe).
+ busyStreak := 0
for {
select {
case <-ctx.Done():
@@ -206,9 +213,24 @@ func (s *Service) workerLoop(ctx context.Context, workerID int) {
if ctx.Err() != nil || errors.Is(err, sql.ErrConnDone) {
return
}
+ if isBusyErr(err) {
+ busyStreak++
+ if busyStreak == 1 || time.Duration(busyStreak)*s.pollEvery >= time.Minute {
+ s.logger.Warn("jobs: claim contended (SQLITE_BUSY), retrying each tick",
+ "worker", workerID, "consecutive", busyStreak)
+ if busyStreak > 1 {
+ busyStreak = 1 // restart the heartbeat window
+ }
+ }
+ continue
+ }
s.logger.Error("jobs: claim failed", "worker", workerID, "err", err)
continue
}
+ if busyStreak > 0 {
+ s.logger.Info("jobs: claim contention cleared", "worker", workerID)
+ busyStreak = 0
+ }
if job == nil {
continue
}
@@ -216,6 +238,16 @@ func (s *Service) workerLoop(ctx context.Context, workerID int) {
}
}
+// isBusyErr reports whether err is SQLite's BUSY/LOCKED contention. modernc's
+// driver has no stable exported sentinel for it, so match the rendered code.
+func isBusyErr(err error) bool {
+ if err == nil {
+ return false
+ }
+ msg := err.Error()
+ return strings.Contains(msg, "SQLITE_BUSY") || strings.Contains(msg, "SQLITE_LOCKED")
+}
+
// recoverOrphanedJobs requeues jobs stuck in 'running' from a previous
// process that crashed or was killed mid-execute. The normal lifecycle
// always moves running → completed/failed/pending, so any 'running' row at
diff --git a/server/internal/projects/projects.go b/server/internal/projects/projects.go
index 47f1e7d..64b1e60 100644
--- a/server/internal/projects/projects.go
+++ b/server/internal/projects/projects.go
@@ -340,24 +340,22 @@ func SetStatus(ctx context.Context, db *sql.DB, hostPath, status string) error {
//
// chunks_meta and chunks_fts are not bound to projects via FK because
// chunks_fts is a virtual table and cannot participate in foreign keys.
-// We wipe them in the same tx that drops the projects row so a failure
-// rolls back the partial state.
+// The FTS wipe runs FIRST, in bounded batches (its own short transactions),
+// because a big project's trigram-FTS delete can take minutes and SQLite has a
+// single writer — one monolithic tx here starved every concurrent writer (see
+// chunksfts.DeleteByProject). Failure midway leaves the projects row intact, so
+// a retried Delete resumes the wipe; FTS rows never outlive the project row.
func Delete(ctx context.Context, db *sql.DB, hostPath string) error {
if _, err := Get(ctx, db, hostPath); err != nil {
return err
}
- tx, err := db.BeginTx(ctx, nil)
- if err != nil {
- return fmt.Errorf("begin delete tx: %w", err)
- }
- defer tx.Rollback() //nolint:errcheck // no-op after commit
- if err := chunksfts.DeleteByProjectTx(ctx, tx, hostPath); err != nil {
+ if err := chunksfts.DeleteByProject(ctx, db, hostPath); err != nil {
return err
}
- if _, err := tx.ExecContext(ctx, `DELETE FROM projects WHERE host_path = ?`, hostPath); err != nil {
+ if _, err := db.ExecContext(ctx, `DELETE FROM projects WHERE host_path = ?`, hostPath); err != nil {
return fmt.Errorf("delete project: %w", err)
}
- return tx.Commit()
+ return nil
}
// ---------------------------------------------------------------------------
diff --git a/server/internal/runtimecfg/runtimecfg.go b/server/internal/runtimecfg/runtimecfg.go
index 7642c40..a16a40a 100644
--- a/server/internal/runtimecfg/runtimecfg.go
+++ b/server/internal/runtimecfg/runtimecfg.go
@@ -41,6 +41,8 @@ const (
FieldMaxEmbeddingConcurrency = "max_embedding_concurrency"
FieldLlamaBatchSize = "llama_batch_size"
FieldIndexEmbedBatchChunks = "index_embed_batch_chunks"
+ FieldChunkMaxConcurrent = "chunk_max_concurrent"
+ FieldLlamaCacheRAMMiB = "llama_cache_ram_mib"
)
// Snapshot is a fully-resolved runtime config — every field is populated, no
@@ -54,6 +56,8 @@ type Snapshot struct {
MaxEmbeddingConcurrency int
LlamaBatchSize int
IndexEmbedBatchChunks int
+ ChunkMaxConcurrent int
+ LlamaCacheRAMMiB int
// Source maps Field* constants to one of SourceDB/SourceEnv/SourceRecommended.
Source map[string]string
@@ -79,6 +83,8 @@ type Patch struct {
MaxEmbeddingConcurrency *int
LlamaBatchSize *int
IndexEmbedBatchChunks *int
+ ChunkMaxConcurrent *int
+ LlamaCacheRAMMiB *int
}
// Service resolves runtime config from the DB, falling through to env-loaded
@@ -113,7 +119,12 @@ func (s *Service) Recommended() Snapshot {
MaxEmbeddingConcurrency: 5,
LlamaBatchSize: 2048,
IndexEmbedBatchChunks: 64,
- Source: map[string]string{},
+ ChunkMaxConcurrent: 3,
+ // 0 = prompt cache disabled. llama-server's own default is 8192 MiB,
+ // which is pure host-RAM waste for embeddings (no prompt reuse) and
+ // OOM-killed the prod container; see the supervisor's --cache-ram note.
+ LlamaCacheRAMMiB: 0,
+ Source: map[string]string{},
}
}
@@ -125,6 +136,8 @@ type dbRow struct {
maxEmbeddingConcurrency sql.NullInt64
llamaBatchSize sql.NullInt64
indexEmbedBatchChunks sql.NullInt64
+ chunkMaxConcurrent sql.NullInt64
+ llamaCacheRAMMiB sql.NullInt64
updatedAt sql.NullString
updatedBy sql.NullString
}
@@ -134,12 +147,14 @@ func (s *Service) loadRow(ctx context.Context) (dbRow, bool, error) {
err := s.db.QueryRowContext(ctx, `
SELECT embedding_model, llama_ctx_size, llama_n_gpu_layers,
llama_n_threads, max_embedding_concurrency, llama_batch_size,
- index_embed_batch_chunks, updated_at, updated_by
+ index_embed_batch_chunks, chunk_max_concurrent, llama_cache_ram_mib,
+ updated_at, updated_by
FROM runtime_settings WHERE id = 1
`).Scan(
&r.embeddingModel, &r.llamaCtxSize, &r.llamaNGpuLayers,
&r.llamaNThreads, &r.maxEmbeddingConcurrency, &r.llamaBatchSize,
- &r.indexEmbedBatchChunks, &r.updatedAt, &r.updatedBy,
+ &r.indexEmbedBatchChunks, &r.chunkMaxConcurrent, &r.llamaCacheRAMMiB,
+ &r.updatedAt, &r.updatedBy,
)
if errors.Is(err, sql.ErrNoRows) {
return dbRow{}, false, nil
@@ -182,6 +197,8 @@ func (s *Service) Get(ctx context.Context) (Snapshot, error) {
out.MaxEmbeddingConcurrency = resolveInt(row.maxEmbeddingConcurrency, hasRow, envIntOrZero(s.env, "conc"), rec.MaxEmbeddingConcurrency, &out.Source, FieldMaxEmbeddingConcurrency)
out.LlamaBatchSize = resolveInt(row.llamaBatchSize, hasRow, envIntOrZero(s.env, "batch"), rec.LlamaBatchSize, &out.Source, FieldLlamaBatchSize)
out.IndexEmbedBatchChunks = resolveInt(row.indexEmbedBatchChunks, hasRow, envIntOrZero(s.env, "idxbatch"), rec.IndexEmbedBatchChunks, &out.Source, FieldIndexEmbedBatchChunks)
+ out.ChunkMaxConcurrent = resolveInt(row.chunkMaxConcurrent, hasRow, envIntOrZero(s.env, "chunkconc"), rec.ChunkMaxConcurrent, &out.Source, FieldChunkMaxConcurrent)
+ out.LlamaCacheRAMMiB = resolveInt(row.llamaCacheRAMMiB, hasRow, envIntOrZero(s.env, "cacheram"), rec.LlamaCacheRAMMiB, &out.Source, FieldLlamaCacheRAMMiB)
if hasRow {
if row.updatedAt.Valid {
@@ -247,6 +264,10 @@ func envIntOrZero(env *config.Config, which string) int {
return env.LlamaBatchSize
case "idxbatch":
return env.IndexEmbedBatchChunks
+ case "chunkconc":
+ return env.ChunkMaxConcurrent
+ case "cacheram":
+ return env.LlamaCacheRAMMiB
}
return 0
}
@@ -287,6 +308,8 @@ func (s *Service) Set(ctx context.Context, patch Patch, updatedBy string) error
mergeInt(&merged.maxEmbeddingConcurrency, patch.MaxEmbeddingConcurrency)
mergeInt(&merged.llamaBatchSize, patch.LlamaBatchSize)
mergeInt(&merged.indexEmbedBatchChunks, patch.IndexEmbedBatchChunks)
+ mergeInt(&merged.chunkMaxConcurrent, patch.ChunkMaxConcurrent)
+ mergeInt(&merged.llamaCacheRAMMiB, patch.LlamaCacheRAMMiB)
now := time.Now().UTC().Format(time.RFC3339Nano)
if hasRow {
@@ -294,26 +317,30 @@ func (s *Service) Set(ctx context.Context, patch Patch, updatedBy string) error
UPDATE runtime_settings
SET embedding_model = ?, llama_ctx_size = ?, llama_n_gpu_layers = ?,
llama_n_threads = ?, max_embedding_concurrency = ?, llama_batch_size = ?,
- index_embed_batch_chunks = ?, updated_at = ?, updated_by = ?
+ index_embed_batch_chunks = ?, chunk_max_concurrent = ?,
+ llama_cache_ram_mib = ?, updated_at = ?, updated_by = ?
WHERE id = 1
`,
nullStr(merged.embeddingModel), nullInt(merged.llamaCtxSize),
nullInt(merged.llamaNGpuLayers), nullInt(merged.llamaNThreads),
nullInt(merged.maxEmbeddingConcurrency), nullInt(merged.llamaBatchSize),
- nullInt(merged.indexEmbedBatchChunks), now, updatedBy,
+ nullInt(merged.indexEmbedBatchChunks), nullInt(merged.chunkMaxConcurrent),
+ nullInt(merged.llamaCacheRAMMiB), now, updatedBy,
)
} else {
_, err = s.db.ExecContext(ctx, `
INSERT INTO runtime_settings (
id, embedding_model, llama_ctx_size, llama_n_gpu_layers,
llama_n_threads, max_embedding_concurrency, llama_batch_size,
- index_embed_batch_chunks, updated_at, updated_by
- ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ index_embed_batch_chunks, chunk_max_concurrent, llama_cache_ram_mib,
+ updated_at, updated_by
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
nullStr(merged.embeddingModel), nullInt(merged.llamaCtxSize),
nullInt(merged.llamaNGpuLayers), nullInt(merged.llamaNThreads),
nullInt(merged.maxEmbeddingConcurrency), nullInt(merged.llamaBatchSize),
- nullInt(merged.indexEmbedBatchChunks), now, updatedBy,
+ nullInt(merged.indexEmbedBatchChunks), nullInt(merged.chunkMaxConcurrent),
+ nullInt(merged.llamaCacheRAMMiB), now, updatedBy,
)
}
if err != nil {
@@ -364,4 +391,6 @@ func (snap Snapshot) ApplyTo(env *config.Config) {
env.MaxEmbeddingConcurrency = snap.MaxEmbeddingConcurrency
env.LlamaBatchSize = snap.LlamaBatchSize
env.IndexEmbedBatchChunks = snap.IndexEmbedBatchChunks
+ env.ChunkMaxConcurrent = snap.ChunkMaxConcurrent
+ env.LlamaCacheRAMMiB = snap.LlamaCacheRAMMiB
}