Bug fixes for code-memory v1.0.32 discovered while indexing a large production TypeScript monorepo.
Upstream: github.com/kapillamba4/code-memory
Patched version: 1.0.32
Test corpus: openclaw/openclaw — 17,212 source files, 945 doc files, ~17k TypeScript/JavaScript + Swift/Kotlin/Go/YAML across 60+ extensions
Hardware: HP Z640, NVIDIA RTX 5060 Ti (16 GB VRAM), CUDA 13.2, 22-core Xeon
This repo documents the discovery and fix process for three bugs in code-memory that only surface at scale. All three were encountered while attempting to index the openclaw/openclaw monorepo — a large multi-platform messaging/AI gateway codebase.
An initial CPU indexing run was launched on the Z640 workstation (22-core Xeon). After approximately 16 hours, the MCP connection closed before the result returned and the process was abandoned. The root cause was a combination of:
- The MCP stdio connection closing when the calling session ended — the tool has no fire-and-forget mode
- The indexer crashing with
UNIQUE constraint failedbefore completing DB writes anyway - The parallel parser workers hitting
sqlite3.InterfaceError: bad parameter or other API misuseon roughly half the files, silently skipping them
After switching to GPU (RTX 5060 Ti), the embedding phase that caused the 16-hour runtime dropped to ~8 minutes. But all three bugs still had to be fixed before the index completed cleanly.
Final successful run: 17,212 code files + 945 doc files in ~59 minutes, producing a 750 MB SQLite database with 111,000 symbols.
File: code_memory/parser.py
Symptom: sqlite3.InterfaceError: bad parameter or other API misuse on roughly 30% of files during the parallel parse phase. The error appeared in logs as NoneType: None — a secondary logging bug (see Bug 4) that masked the real exception.
Root cause: _parse_file_for_indexing runs inside a ThreadPoolExecutor worker but calls db.execute() (a freshness check SELECT) on the shared SQLite connection that was created on the main thread. Although the connection is opened with check_same_thread=False, Python's sqlite3 binding is not safe for concurrent access from multiple threads without locking — simultaneous SELECTs from 4 workers caused internal state corruption.
Fix: Pre-fetch all existing file records into a dict[path → mtime] in the main thread before launching the thread pool. Pass this dict to _parse_file_for_indexing as existing_mtimes. Workers do a dict lookup instead of a DB query. No DB access occurs in any worker thread.
# Before (in thread worker — unsafe):
row = db.execute(
"SELECT id, last_modified FROM files WHERE path = ?", (filepath,)
).fetchone()
# After (in main thread before pool launch):
existing_mtimes = {
os.path.abspath(row[0]): row[1]
for row in db.execute("SELECT path, last_modified FROM files").fetchall()
}
# Worker receives dict, does dict.get(filepath) insteadImpact on openclaw: ~7 files per 337 in extensions/telegram were skipped before the fix. After the fix, all 337 parsed successfully.
File: code_memory/parser.py
Symptom: sqlite3.IntegrityError: UNIQUE constraint failed: symbols.file_id, symbols.name, symbols.kind, symbols.line_start during the sequential DB write phase (Phase 3), after all parsing and GPU embedding had completed.
Root cause: tree-sitter's AST extraction can produce duplicate symbol entries for a single file — multiple nodes with the same (name, kind, line_start) tuple. The INSERT INTO symbols was a plain insert with no conflict handling, so the second occurrence raised and killed the entire indexing run.
Fix: Change to INSERT OR IGNORE and detect whether the insert actually happened using cursor.rowcount == 1 (not cursor.lastrowid, which is unreliable after an ignored insert — it retains the ID from the previous successful insert).
# Before:
cursor = db.execute("INSERT INTO symbols (...) VALUES (...)", ...)
sym_id = cursor.lastrowid
# After:
cursor = db.execute("INSERT OR IGNORE INTO symbols (...) VALUES (...)", ...)
is_new = cursor.rowcount == 1
sym_id = cursor.lastrowid if is_new else db.execute(
"SELECT id FROM symbols WHERE file_id=? AND name=? AND kind=? AND line_start=?",
(file_id, sym["name"], sym["kind"], sym["line_start"]),
).fetchone()[0]The is_new flag is also threaded through to Bug 3 below.
Note on cursor.lastrowid: After INSERT OR IGNORE that ignores a row, lastrowid is not reset to 0 — it retains the rowid from the last successful insert on that connection. Using bool(cursor.lastrowid) to detect a new insert is therefore wrong; cursor.rowcount == 1 is the correct test.
File: code_memory/parser.py, code_memory/db.py
Symptom: sqlite3.OperationalError: UNIQUE constraint failed on symbol_embeddings primary key immediately after the Bug 2 fix was applied.
Root cause: symbol_embeddings is a sqlite-vec virtual table, not a regular SQLite table. Virtual tables do not support INSERT OR IGNORE — the conflict resolution clause is silently ignored and the underlying virtual table module raises OperationalError instead of IntegrityError. When a duplicate symbol was skipped by Bug 2's INSERT OR IGNORE, we fell back to the existing symbol_id via SELECT — and then still queued an embedding for that ID. The existing row in symbol_embeddings already had an embedding for that ID, causing the crash.
Fix: Only queue an embedding when is_new is True — i.e., only for symbols that were freshly inserted, not ones retrieved from the fallback SELECT.
# Before:
if file_embeddings and i < len(file_embeddings):
embedding_pairs.append((sym_id, file_embeddings[i]))
# After:
if is_new and file_embeddings and i < len(file_embeddings):
embedding_pairs.append((sym_id, file_embeddings[i]))Why not INSERT OR IGNORE on symbol_embeddings? sqlite-vec virtual tables reject conflict resolution clauses at the SQL level. The only safe approach is to never attempt a duplicate insert.
File: code_memory/parser.py
Symptom: All parse failures logged as NoneType: None with no traceback — making Bug 1 completely invisible in logs.
Root cause: Exceptions from worker threads are stored as return values: return (fpath, None, e). In the main thread, logger.exception("Failed to index %s", fpath) is called with no exc_info argument. logger.exception() reads from sys.exc_info() — the current thread's exception context — which is (None, None, None) at that point since the exception occurred in a different thread. Result: every failure logs as NoneType: None.
Fix: Pass the stored exception explicitly:
# Before:
logger.exception("Failed to index %s", fpath)
# After:
logger.error("Failed to index %s", fpath, exc_info=error)Not a code fix — a repo configuration issue.
Symptom: sqlite3.IntegrityError: UNIQUE constraint failed: symbols.file_id, symbols.name, symbols.kind, symbols.line_start on first run against openclaw, before any other bugs were triggered.
Root cause: The openclaw/openclaw repo uses symlinks to alias CLAUDE.md → AGENTS.md in five subdirectories (/, extensions/, scripts/, docs/, ui/). code-memory resolves symlinks to canonical paths and maps them to the same file_id. Indexing both the symlink and its target for the same physical file produced duplicate (file_id, name, kind, line_start) tuples before Bug 2's INSERT OR IGNORE was in place.
Fix: Add CLAUDE.md to .git/info/exclude in the openclaw repo (local exclude, not tracked .gitignore):
# .git/info/exclude
CLAUDE.md
Bug 2's INSERT OR IGNORE would now handle this case even without the exclude, but the exclude is cleaner.
A standalone Python script (run-code-memory-index.py) is included to work around the MCP connection timeout problem.
Problem: index_codebase is an MCP tool call that blocks until the entire index is written. On a 17k-file repo, this takes 45–60 minutes. Claude Code's MCP stdio connection does not survive that long — the connection closes, the result is never returned, and the partial DB is abandoned. This was the root cause of the 16-hour overnight CPU failure: the process kept being restarted from scratch rather than resuming.
Solution: The script imports directly from the code_memory package, bypassing the MCP layer entirely. Run it under nohup and it will survive session drops, reboots of the MCP server, and anything else that would kill a stdio connection.
nohup ~/.local/share/uv/tools/code-memory/bin/python run-code-memory-index.py /path/to/repo \
> ~/code-memory-index.log 2>&1 &Monitor:
tail -f ~/code-memory-index.log
watch -n 5 'ls -lh /path/to/repo/code_memory.db; nvidia-smi --query-gpu=utilization.gpu,memory.used --format=csv,noheader'| Phase | CPU (22-core Xeon) | GPU (RTX 5060 Ti) |
|---|---|---|
| tree-sitter parsing (17,212 files) | ~2 min | ~2 min |
| Embedding generation (17,212 files) | ~14+ hours (estimated) | ~8 min |
| DB write | — (never reached) | ~45 min |
| Total | never finished | ~59 min |
The CPU run never finished because embedding generation with sentence-transformers on a 0.5B parameter model is GPU-bound. The 22-core Xeon is not meaningfully faster than a single GPU for batched transformer inference. VRAM usage during embedding: ~1.3 GB active PyTorch allocations, ~3.8 GB CUDA reserved.
Important: Do not run two code-memory processes simultaneously. The first process loads the embedding model and claims CUDA memory. The second process will OOM attempting to load a second model instance. Kill the first process before starting the second.
| File | Description |
|---|---|
parser.py |
Patched — Bugs 1, 2, 3, 4 fixed |
db.py |
Patched — Bug 3 guard (reverted INSERT OR IGNORE on virtual table) |
run-code-memory-index.py |
Standalone indexer that bypasses MCP connection timeout |
PATCHES.md |
Unified diff summary of all changes |
These files are drop-in replacements for the installed package files. Find your install:
find ~/.local -name "parser.py" -path "*/code_memory/*" 2>/dev/nullThen copy:
cp parser.py ~/.local/share/uv/tools/code-memory/lib/python3.13/site-packages/code_memory/parser.py
cp db.py ~/.local/share/uv/tools/code-memory/lib/python3.13/site-packages/code_memory/db.pyNote: These patches will be overwritten if you uvx update code-memory. Re-apply after any upstream upgrade.
Bug report / PR candidate: https://github.com/kapillamba4/code-memory
All four bugs are reproducible on any large TypeScript monorepo. The threading bug (Bug 1) affects any codebase with more files than the thread pool workers can process before the second worker finishes its first file — in practice, any repo larger than ~20 files.