Regenerates the committed dist from the combined source of the merged fixes, normalizing long-standing bundle drift (stale version, older vendored deps) so dist reproducibly matches src.
Claude-Session: https://claude.ai/code/session_0112vdwZphiWzfCYfaXMes4C
Independent SessionStart events from multiple Claude Code sessions
each fire `episodic-memory sync --background`, which forks a detached
worker. Without coordination, N parents trigger N concurrent worker
processes racing the same archive and SQLite database.
The reentrancy guard (#87) covers same-process recursion, but it
can't stop SessionStart events from independent sessions whose envs
don't carry EPISODIC_MEMORY_SUMMARIZER_GUARD.
Reproduced locally on macOS: 3 parallel `node dist/sync-cli.js`
processes against the same TEST_ARCHIVE_DIR — worker 1 completes;
workers 2 and 3 crash with `SqliteError: database is locked`
(SQLITE_BUSY) trying to init the DB. On Windows, the reporter's
setup with ~67 worktrees instead piles up enough claude.exe children
to exhaust the desktop heap and crash with STATUS_DLL_INIT_FAILED
(0xC0000142). Same root cause; different blast radius depending on
how far the workers get before stepping on each other.
Fix: a single-instance lock around the sync worker, implemented as a
thin wrapper in src/file-lock.ts over the `proper-lockfile` package.
A first attempt at a hand-rolled openSync('wx') + PID-file protocol
had a residual race under concurrent stale-stealers that pure file
primitives cannot fully close without advisory locking.
proper-lockfile uses an atomic-mkdir + mtime-heartbeat protocol that
is race-free under that contention shape — the same approach npm
itself uses.
sync-cli.ts acquires <log-dir>/episodic-memory-sync.lock after the
source-dir check and before initDatabase(); if another live process
holds it, the worker prints "sync already running (pid X); skipping"
to stderr and exits 0. The lock releases on normal exit and on the
common signals (SIGINT/SIGTERM/SIGHUP).
Embedding migration's own lock now delegates to the generic helper;
its old export names (acquireMigrationLock, releaseMigrationLock,
MigrationLockHandle) stay for back-compat.
proper-lockfile is excluded from the esbuild bundle (runtime dep on
the same level as better-sqlite3/transformers/etc.) so the MCP
server bundle size is unchanged. The wrapper's install-health probe
(#95 Bug 1) gains proper-lockfile as a required package so a partial
extraction surfaces a useful diagnostic.
Tests:
- test/file-lock.test.ts: acquire/release, contention,
parent-dir creation, garbage diagnostic content, I/O error
propagation, N-concurrent-acquirers stress test, mtime-based
stale recovery, fresh-lock-not-reclaimable. Subprocess imports
use pathToFileURL for Windows compatibility.
- test/sync-cli-single-instance.test.ts: integration via real
child-process spawn — two concurrent workers (one completes,
one skips), single sequential run still works, lock released
on normal exit, stale lock from a dead PID is reclaimable.
- Existing test/embedding-migration.test.ts continues to pass.
Closes#97.
Upgraded the embedding model from all-MiniLM-L6-v2 (Xenova transformers
2.x, ORT 1.14) to bge-small-en-v1.5 (HuggingFace transformers 4.x,
ORT 1.24). Measured +6.34 R@1 absolute improvement on a 17K-corpus
retrieval test against real production data — 46.8% to 53.1%, about
a 13.5% relative lift on "right answer first." R@5 +7.1, R@10 +6.7,
MRR +0.067.
Same 384-dim embeddings, so vec_exchanges FLOAT[384] is unchanged.
No bigger model performed better on this content — bge-base, gte-small,
nomic-embed-text-v1.5 (with and without long context), and various
prefix/HyDE/reranking combinations all tested and rejected. Cross-
encoder reranking specifically: zero measurable lift on top of bge-
small for this corpus.
Asymmetric encoding: queries get the BGE retrieval prefix
("Represent this sentence for searching relevant passages: ");
docs stay unmodified. Production was already calling generateEmbedding
directly for queries vs generateExchangeEmbedding for docs, so adding
generateQueryEmbedding splits cleanly along the existing seam.
Auto-migration:
- New embedding_version column tracks which encoder produced each row.
- sync-cli runs runMigrationBatch(500) after normal sync; batched and
resumable via per-row version tag.
- File-based lock (~/.config/superpowers/conversation-index/
.embedding-migration.lock) with PID-liveness for stale recovery.
Concurrent sync processes that find the lock held just skip their
migration phase; their next sync picks up.
- Single transaction per batch for durability. Crash mid-batch leaves
unfinished rows tagged for migration, picked up next time.
- Search works throughout: vec_exchanges holds a mix of old- and
new-encoder vectors during the transition window. Cosine similarity
is mathematically defined across them; ranking degrades gracefully.
- ~7 min cumulative background work on a 30K-exchange index, spread
across however many syncs are triggered.
Resolves#82 incidentally — the new ORT 1.24 fixes the protobuf-
parsing crash that broke Node ≤23 setups.
Tests: 10 new (3 query-prefix, 7 migration including end-to-end with
the real encoder + concurrent-lock simulation). Test count 108 → 118.
The persistSession: false fix in 1.1.0 (#83) only stopped the
SDK-spawned Claude subprocess from saving its session JSONL — it did
not stop the subprocess from firing SessionStart hooks. The plugin's
own SessionStart hook ran 'episodic-memory sync --background', which
re-summarized, which spawned another Claude subprocess, which fired
the hook again. Reporters saw hundreds of detached processes, CPU
saturated, and API quota burning. Multiple users affected.
Fix: a reentrancy-guard env var EPISODIC_MEMORY_SUMMARIZER_GUARD set
when calling the SDK's query(). The SDK passes our env to the spawned
subprocess. The sync-cli entry point checks the guard at startup and
exits silently when it's set, breaking the recursion at the only
intervention point we control.
Tests:
- getApiEnv() always sets the guard
- shouldSkipReentrantSync() reads it correctly
- Spawning dist/sync-cli.js with the guard env exits 0 without doing
any sync work, even with --background
Anyone running 1.1.0 or 1.1.1 should upgrade immediately.
- Add --background flag to sync command that forks process and returns immediately
- Update SessionStart hook to use --background mode
- Maintains full synchronous mode for manual usage
- Resolves startup blocking issue while preserving async: true hook setting
Usage:
- Manual sync: episodic-memory sync (runs synchronously)
- Hook sync: episodic-memory sync --background (returns immediately)
This ensures Claude Code startup is never blocked by sync operations
while still allowing full sync functionality when run manually.
Fix MCP server startup error by committing pre-built JavaScript files.
The dist/ directory is now tracked in git to ensure built files are
immediately available after plugin installation without requiring
a build step.
Changes:
- Remove dist/ from .gitignore
- Pre-build and commit all dist/ files
- Bump version to 1.0.2 in package.json, plugin.json, marketplace.json
- Update CHANGELOG.md
Fixes: Error: Cannot find module 'dist/mcp-server.js'