Skip to content

fix(agentdb): cache prepared statements to plug sql.js leak#144

Merged
ruvnet merged 1 commit into
mainfrom
fix/agentdb-statement-cache
May 2, 2026
Merged

fix(agentdb): cache prepared statements to plug sql.js leak#144
ruvnet merged 1 commit into
mainfrom
fix/agentdb-statement-cache

Conversation

@ruvnet

@ruvnet ruvnet commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

Both sql.js wrappers in packages/agentdb/src/db-fallback.ts tracked every prepare() call in activeStatements but only removed entries on finalize() or error. The idiomatic better-sqlite3 pattern db.prepare(sql).run(...) — which relies on V8 to GC the handle — leaks unbounded under sql.js because WASM-backed Statement objects need explicit .free().

Observed in mcp-bridge Cloud Run logs (~3 statements/s growth):

[ruflo] ⚠️ Detected 562 active SQL statements - possible memory leak
[ruflo] ⚠️ Detected 612 active SQL statements - possible memory leak
[ruflo] ⚠️ Detected 621 active SQL statements - possible memory leak

Fix

Cache wrappers by SQL text. Repeated prepare(sameSql) returns the same wrapper; activeStatements.size is now bounded by the count of distinct SQL strings the application uses.

  • Safe under single-threaded JS: bind/step/reset are synchronous, so no two run/get/all calls can interleave on a shared stmt.
  • finalize() is a no-op on cached wrappers; close() owns teardown.
  • Auto-free-on-error also evicts from the cache, so a broken stmt isn't reused.

Applied identically to both SqlJsDatabase (line ~94) and wrapExistingSqlJsDatabase (line ~305).

Test plan

  • tsc --noEmit clean
  • Smoke test — 5000× db.prepare('INSERT...').run() + 1× db.prepare('SELECT').get():
    activeStatements_size: 2   (was 5001 pre-patch)
    statementCache_size:   2
    post_close_active:     0
    post_close_cache:      0
    
  • Pre-existing test failures (jest stubs in vitest project, hnsw zero-vector edge case) — unrelated to this change.

Out of scope

  • npm publish + downstream redeploy of mcp-bridge Cloud Run service. The fix won't reach production until consumers pick up a published version.

🤖 Generated with claude-flow

Both sql.js wrappers in db-fallback.ts (SqlJsDatabase and
wrapExistingSqlJsDatabase) tracked every prepare() call in
activeStatements and only removed entries on finalize() or error.
The idiomatic better-sqlite3 pattern db.prepare(sql).run(...) — which
relies on V8 to GC the handle — leaks unbounded under sql.js because
WASM-backed Statement objects need explicit .free().

Production observation (Cloud Run mcp-bridge): ~3 statements/s growth,
hit 621 within minutes:
  [ruflo] ⚠️ Detected 562 active SQL statements - possible memory leak
  [ruflo] ⚠️ Detected 612 active SQL statements - possible memory leak
  [ruflo] ⚠️ Detected 621 active SQL statements - possible memory leak

Fix: cache wrappers by SQL text. Repeated prepare(sameSql) returns the
same wrapper; activeStatements.size is now bounded by the count of
distinct SQL strings. Safe under single-threaded JS — bind/step/reset
are synchronous so no two run/get/all calls can interleave on a shared
stmt. finalize() is now a no-op on cached wrappers (close() owns
teardown); auto-free-on-error also evicts from cache so a broken stmt
isn't reused.

Smoke test (5000 prepare(INSERT).run + 1 prepare(SELECT).get):
  activeStatements_size: 2  (was 5001 pre-patch)
  statementCache_size:   2
  post_close_active:     0
  post_close_cache:      0

Typecheck: clean.
Test failures observed in repo are pre-existing (jest stubs in vitest
project, hnsw zero-vector edge case) — unrelated to this change.

Co-Authored-By: claude-flow <ruv@ruv.net>
@ruvnet
ruvnet merged commit f31065c into main May 2, 2026
13 of 15 checks passed
@ruvnet
ruvnet deleted the fix/agentdb-statement-cache branch May 2, 2026 04:24
sparkling added a commit to sparkling/agentic-flow that referenced this pull request May 3, 2026
Upstream commit f31065c: fix(agentdb): cache prepared statements to plug
sql.js leak. Relevant despite our RVF-primary path because SQLite is
documented fallback per ADR-0110.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant