feat: upgrade memory to memory-lancedb-pro (hybrid BM25+vector retrieval)#1043
feat: upgrade memory to memory-lancedb-pro (hybrid BM25+vector retrieval)#10435queezer wants to merge 3 commits intoqwibitai:mainfrom
Conversation
Detailed code review covering memory-store.ts, memory-retriever.ts, memory-embedder.ts, and supporting changes. Identifies 2 critical issues (missing chunker.js, Float32Array in migration script), 7 high-severity findings, and additional medium/low items. https://claude.ai/code/session_01XgxQodZdoZxczUa4iZv9vU
5queezer
left a comment
There was a problem hiding this comment.
Second Review — Remaining Issues
The previous critical and high findings were addressed. However this round of review uncovered new issues, including regressions introduced by some of the fixes.
Critical (3)
1. update() add-before-delete creates a concurrent-read duplicate window
memory-store.ts
The crash-safe ordering (add first, then delete) is correct for durability, but it introduces a window where two copies of the same logical record exist simultaneously. Any vectorSearch or bm25Search running during that window returns both entries. Since MCP tool calls are async and concurrent, this window is reachable in production. If the process crashes after add but before delete, the stale duplicate persists forever.
Suggested fix: expose a version field and filter ORDER BY version DESC LIMIT 1 in reads, or implement a batchHasIds method on MemoryStore and tombstone the old entry.
2. update() full-UUID path does not explicitly select all columns
memory-store.ts
The prefix path selects all columns explicitly. The full-UUID path does not. If LanceDB returns a row without vector, the subsequent updates.vector ?? Array.from(row.vector as Iterable<number>) silently produces a zero-vector. Add .select([...all fields...]) on both paths.
3. Ghost check bypasses encapsulation via (this.store as any).table
memory-retriever.ts
This breaks MemoryStore encapsulation, bypasses TypeScript, and silently returns [] (causing all BM25-only results to pass as live — the opposite of the intended behavior) if table is null before ensureInitialized completes. MemoryStore should expose a public batchHasIds(ids: string[]): Promise<Set<string>> method.
High (5)
4. embedMany chunking path missing L2-normalization
memory-embedder.ts
The fix for embedSingle added L2-normalization after averaging chunk embeddings. The equivalent path in embedMany was missed. Vectors stored via the batch path will have different magnitudes, causing incorrect cosine similarity scores.
5. Per-slot [] fallback in embedMany success path not fixed
memory-embedder.ts
The all-invalid path was fixed to return zero vectors. But inside the success path, individual invalid slots still fall through to results[i] = []. A caller passing a mixed array gets a zero-length vector for empty slots, which table.add() will accept silently, corrupting the entry.
6. Inline SQL escape in ghost check is inconsistent
memory-retriever.ts
The ghost check builds its own IN clause with id.replace(/'/g, "''") instead of using the shared escapeSqlLiteral. The result is equivalent but the inconsistency is dangerous. Use escapeSqlLiteral or add batchHasIds to MemoryStore.
7. Raw error object logged on rerank failure — potential API key leak
memory-retriever.ts
Some HTTP client errors include full request headers in their stringified form. Log error instanceof Error ? error.message : String(error) instead of the raw error object.
8. list() and stats() load entire table for pagination
memory-store.ts
Both methods call .toArray() on the full table before slicing. At 1000+ entries with 3072-float vectors this is an OOM risk. LanceDB supports .offset(n).limit(k) — use it.
Medium (6)
9. event and general categories silently stored as "other"
memory.ts + ipc-mcp-stdio.ts
The MCP tool exposes event and general as valid category values. normalizeCategory maps both to "other". A memory stored with category: "event" will never be found by a search with category: "event".
10. AccessTracker is in-memory only — reinforcement feature non-functional across restarts
Access counts reset on every container restart. Access metadata should be persisted to the metadata JSON column, or the feature documented as session-scoped.
11. migrate-memories.mjs buffers all records before writing
All vectors accumulated in memory before createTable. At 10k entries ~240MB for vectors alone. Batch writes every 100 records.
12. migrate-memories.mjs uses Gemini native API instead of OpenAI-compatible path
The main memory module uses the OpenAI-compatible Gemini endpoint. The migration script uses embedContent directly. These may return vectors with different normalization, causing wrong cosine similarity scores for migrated entries.
13. Chunker guard miscalculated when overlapSize >= maxChunkSize
Caps iteration at 4 chunks regardless of document size. Add validation that overlapSize < maxChunkSize.
14. update() preserves original timestamp — defeats recency boost for corrected memories
Add an updatedAt field or at minimum document this limitation.
Low (4)
rerankProvidermissing fromDEFAULT_RETRIEVAL_CONFIGfindLastIndexWithininmemory-chunker.tsis dead codeloadLanceDB()called twice insidedoInitialize- BM25 sigmoid divisor
5is an undocumented magic constant
Previous Review Status
| Item | Status |
|---|---|
| Missing chunker.ts | Resolved |
| Float32Array in migrate | Resolved |
| SQL injection in bulkDelete | Resolved |
| Non-atomic update() | Partially fixed — crash direction correct but concurrent-read window introduced (see #1) |
| N+1 hasId calls | Replaced with batch query — but via any cast (see #3) |
| Stack overflow in embedSingle | Resolved via depth guard |
| embedMany returning [] | Partially fixed — all-invalid path fixed, per-slot case remains (see #5) |
| Misleading RRF naming | Resolved |
4e67441 to
44b0370
Compare
There was a problem hiding this comment.
Round 3 Review — All Critical & High Fixes Verified ✅
Checked commit 44b0370. All 3 critical and 5 high findings from the previous review are addressed.
✅ Previously Critical — Now Fixed
update() full-UUID path missing column select
allColumns array is now passed to .select() at line 727–742 of memory-store.ts.
Vector field is included, update no longer drops the embedding.
ghost check via (this.store as any).table
Replaced with this.store.filterExistingIds(bm25OnlyIds) — proper public API, no more unsafe cast.
update() delete window
Switched to delete-then-add with a clear comment documenting the tradeoff (no duplicates, accepts data loss on crash). Deliberate design decision, acceptable for a memory system.
✅ Previously High — Now Fixed
All 5 high findings resolved:
embedManychunking path now L2-normalizes averaged vectors ✅- per-slot
[]replaced withnew Array(this.dimensions).fill(0)✅ - All scope filter interpolations use
escapeSqlLiteral()✅ - Rerank error logging strips headers/objects, logs message only ✅
list()capped at 1000,stats()capped at 10_000 ✅
🟡 Remaining Medium
1. migrate-memories.mjs — no LanceDB Cloud support
const db = await lancedb.connect(lancedbDir); // line 74No apiKey passed. Migrating into a db:// Cloud table will fail silently or throw.
Fix: pass { apiKey: process.env.LANCEDB_API_KEY } when lancedbDir.startsWith('db://').
2. scope backward-compat check doesn't fix schema
doInitialize() detects missing scope column (lines 208–217) but only warns — does not add the column or re-migrate. Any local installation upgrading from the old schema will get SQL errors on scope = 'global' filters.
Fix: either run the migration automatically, or throw with a clear message pointing to the migration script.
3. RERANK_PROVIDER not auto-detected from available keys
JINA_API_KEY is present but reranking stays disabled unless RERANK_PROVIDER=jina is also set explicitly. Counter-intuitive: the key is there, reranking silently doesn't activate.
Fix: auto-detect provider from available API keys when RERANK_PROVIDER is unset, or at least log a hint like JINA_API_KEY found but RERANK_PROVIDER not set — reranking disabled.
🔵 Low
4. vectorDim: embeddingConfig.dimensions || 0 (memory.ts:187)
If someone sets EMBEDDING_PROVIDER=custom without EMBEDDING_DIM, vectorDim defaults to 0 — LanceDB will create a broken zero-dimensional table before the Embedder throws its descriptive error.
Fix: validate early: if (!dim) throw new Error('Set EMBEDDING_DIM for custom provider').
Summary
| Severity | Count | Status |
|---|---|---|
| Critical | 3 | ✅ All fixed |
| High | 5 | ✅ All fixed |
| Medium | 3 | 🟡 New |
| Low | 2 | 🔵 Minor |
Code is in good shape. The 3 medium items are real but none are blockers for merge — the schema migration warning (item 2) is the most important to address before promoting to production.
44b0370 to
24d016a
Compare
There was a problem hiding this comment.
Round 4 Review — All Medium Findings Fixed ✅
Checked commit 24d016a. All 3 medium findings from the previous review are addressed.
✅ Previously Medium — Now Fixed
migrate-memories.mjs LanceDB Cloud support
Lines 74–77 now check lancedbDir.startsWith('db://') and pass { apiKey } to lancedb.connect(). Fixed.
vectorDim=0 for custom provider
Lines 198–200 now validate early:
if (\!vectorDim) throw new Error('EMBEDDING_DIM must be set for custom provider');Clean error before any LanceDB call.
RERANK_PROVIDER hint
Lines 150–159: when RERANK_PROVIDER is unset but JINA_API_KEY (or other keys) are present, a hint is logged:
JINA_API_KEY is set but RERANK_PROVIDER is not — set RERANK_PROVIDER=jina to enable cross-encoder reranking
scope backward compat (bonus)
Now throws a hard error with migration instructions instead of just warning. Better than the original suggestion.
The endpoint/provider table (Jina, SiliconFlow, Voyage etc.) is fine — it's public documentation of optional config, not private data.
Summary
| Severity | Count | Status |
|---|---|---|
| Critical | 3 | ✅ All fixed |
| High | 5 | ✅ All fixed |
| Medium | 3 | ✅ All fixed |
5queezer
left a comment
There was a problem hiding this comment.
Solid work. This is a meaningful upgrade — the basic cosine-only memory gets replaced with a proper retrieval pipeline that handles real-world edge cases well.
What stands out:
The hybrid scoring design is pragmatic: vector similarity as the base score with BM25 as a confirmation bonus (rather than true RRF) keeps scores interpretable and avoids the ghost-entry problem that pure BM25 fusion creates. The batch ghost-check for stale FTS index entries is a nice touch — one query instead of N.
The chunking + L2-normalize path in the embedder handles long documents correctly. Many implementations average without normalizing, which quietly breaks cosine similarity downstream.
The schema backward-compat check throws a hard error with a migration command instead of silently misbehaving — exactly right.
The multi-key rotation in the embedder with per-key failover on rate limits is production-ready and adds real resilience for high-volume use.
The scoring pipeline (recency boost → importance weight → length norm → time decay → hard cutoff → MMR) is well-layered and each stage is independently configurable, which makes tuning straightforward.
One note:
The PR description still says "RRF fusion" — the actual implementation uses a vector-base + BM25-bonus approach, not Reciprocal Rank Fusion. Worth updating the description to match (it was already corrected in the code comments).
Otherwise: clean, no blocking issues.
Follow-up:
|
5queezer
left a comment
There was a problem hiding this comment.
Code Review — Hermes Agent
Thorough review of all 12 files. Found 5 merge blockers, 11 high/medium issues, and several minor improvements. The overall architecture is solid — hybrid retrieval with good fallback chains, excellent NaN guards, and clean provider abstraction.
Merge Blockers
| # | File | Issue |
|---|---|---|
| 1 | memory.ts:287 |
_distance math is wrong — 1 - score ≠ inverse of 1/(1+d) |
| 2 | memory-store.ts:76 |
SQL injection via scope — escapeSqlLiteral only escapes ' and \ |
| 3 | memory-retriever.ts:734 |
Not actually RRF — it's a +15% BM25 bonus. vectorWeight/bm25Weight config fields are unused dead code |
| 4 | migrate-memories.mjs:20 |
Truncated variable proces..._KEY — script crashes on execution |
| 5 | ipc-mcp-stdio.ts:398 |
memory_delete has no scope parameter — breaks scope isolation model |
High
memory-store.ts:791— Race condition inupdate(): delete then add with data loss window betweenmemory-embedder.ts:37— Cache key truncated to 96 bits (sha256.slice(0,24)) — also missing model name in keymemory-retriever.ts:1043-1044— Length normalization:Math.max(ratio, 1)means short texts get no boost despite docs claiming they domemory-embedder.ts:617— Batch chunking fallback re-embeds ALL texts when only one exceeds context limit
Medium (non-blocking but recommended)
memory-store.ts:686—stats()capped at 10k rows,memoryCount()silently wrong for large tablesmemory-store.ts:807—bulkDeletemissingOR scope IS NULLunlike every other scope filtermemory-access-tracker.ts:76—accessLogMap grows unbounded, no evictionipc-mcp-stdio.ts:415—memory_counthas no scope filter — shows global count across all scopesmemory-retriever.ts:865,918— Cross-encoder blend (60/40) vs cosine fallback (70/30) — different ratios for same purposememory-retriever.ts:885— Unreturned rerank candidates get 0.8x penalty with noscoreHistorytrackingsrc/container-runner.ts— MissingOLLAMA_BASE_URL/OLLAMA_HOSTenv var forwarding — Ollama won't work in containers
What's Good
- Scope parameter correctly exposed in MCP tools ✅
- NaN/Infinity guards throughout the scoring pipeline
- Fail-open on ghost-check errors — right call for retrieval
- API key rotation with rate-limit detection
- Container security: env-file approach,
.envshadow mount - Score tracing system (
scoreHistory) for debugging
Full details in inline comments below.
container/agent-runner/src/memory.ts
Outdated
| importance: r.entry.importance, | ||
| timestamp: r.entry.timestamp, | ||
| metadata: r.entry.metadata ?? '{}', | ||
| _distance: 1 - r.score, |
There was a problem hiding this comment.
🔴 Blocker: Wrong distance conversion
The score was computed as score = 1 / (1 + distance) in memory-store.ts:437.
The inverse is distance = (1 - score) / score, not 1 - score.
Example: if distance = 1.0, then score = 0.5, but 1 - 0.5 = 0.5 instead of the true distance 1.0.
| _distance: 1 - r.score, | |
| _distance: r.score > 0 ? (1 - r.score) / r.score : Infinity, |
There was a problem hiding this comment.
Self-review correction: Downgrading to medium. _distance is only used for display in the MCP tool response (toFixed(3)), not for retrieval ranking. Still wrong math — will fix.
| } | ||
|
|
||
| function escapeSqlLiteral(value: string): string { | ||
| return value.replace(/\\/g, "\\\\").replace(/'/g, "''"); |
There was a problem hiding this comment.
🔴 Blocker: SQL injection risk
This only escapes backslashes and single quotes. LanceDB uses DuckDB SQL under the hood — null bytes, Unicode escapes, or other special chars could bypass this.
Since scope values come directly from user input (MCP tools), this is a real attack surface.
Suggestion: Validate scope values against a whitelist pattern before they reach SQL:
const SCOPE_PATTERN = /^[a-zA-Z0-9_.-]+$/;
if (!SCOPE_PATTERN.test(scope)) throw new Error(`Invalid scope: ${scope}`);Or use parameterized queries if LanceDB supports them.
There was a problem hiding this comment.
Confirmed. Adding scope validation with whitelist pattern.
| // (e.g. searching "JINA_API_KEY") still surface. The previous floor of 0.5 | ||
| // was too generous and allowed ghost entries to survive hardMinScore (0.35). | ||
| const fusedScore = vectorResult | ||
| ? clamp01(vectorScore + bm25Hit * 0.15 * vectorScore, 0.1) |
There was a problem hiding this comment.
🔴 Blocker: This is not RRF
The PR description says "RRF fusion" but this is vectorScore + bm25Hit * 0.15 * vectorScore — a simple additive bonus where BM25 is treated as binary (hit/miss). The actual BM25 score magnitude is completely ignored.
Standard RRF: score = Σ 1/(k + rank_i) for each retriever.
This approach may actually work fine in practice, but:
- The PR description should say "vector-primary + BM25 boost" not "RRF"
vectorWeight(line 22) andbm25Weight(line 23) are defined in config but never used in this fusion — they mislead users into thinking they can tune the blend
Either wire the config weights in, or remove them and update the docs.
There was a problem hiding this comment.
Confirmed. Removing dead vectorWeight/bm25Weight config fields and updating the fusion description to accurately reflect the algorithm.
| import { createInterface } from 'readline'; | ||
| import * as lancedb from '@lancedb/lancedb'; | ||
|
|
||
| const API_KEY = process.env.EMBEDDING_API_KEY || process.env.GEMINI_API_KEY; |
There was a problem hiding this comment.
🔴 Blocker: Truncated variable — script won't run
proces..._KEY is not valid JavaScript. This appears to be a GitHub/API redaction artifact. Should be:
| const API_KEY = process.env.EMBEDDING_API_KEY || process.env.GEMINI_API_KEY; | |
| const API_KEY = process.env.EMBEDDING_API_KEY || process.env.GEMINI_API_KEY; |
There was a problem hiding this comment.
False positive — retracted. The actual source has process.env.EMBEDDING_API_KEY — this is GitHub API secret redaction, not a code bug. Verified with od -c on the real file. Apologies for the noise.
scripts/migrate-memories.mjs
Outdated
| for await (const line of rl) { | ||
| if (!line.trim()) continue; | ||
| total++; | ||
| const entry = JSON.parse(line); |
There was a problem hiding this comment.
A single malformed line in the JSONL file crashes the entire migration. Should wrap in try/catch:
let entry;
try {
entry = JSON.parse(line);
} catch (e) {
console.warn(`Skipping malformed line ${lineNum}: ${e.message}`);
continue;
}There was a problem hiding this comment.
Confirmed. Adding try/catch with skip + warning.
| // to parallel reads. If we crash between delete and add, data is lost for | ||
| // this entry — acceptable for a memory system vs. returning duplicates. | ||
| const resolvedId = escapeSqlLiteral(row.id as string); | ||
| await this.table!.delete(`id = '${resolvedId}'`); |
There was a problem hiding this comment.
If the process crashes between delete and add, the memory entry is permanently lost. Concurrent reads between delete and add will see the entry as missing.
Safer approach: add the updated entry first (accepting brief duplicates), then delete the old one. Duplicates are less harmful than data loss for a memory system.
There was a problem hiding this comment.
Retracted. The code comment at lines 787-789 explicitly documents this as a deliberate design decision: "delete-then-add avoids a window where both old and new rows are visible to parallel reads. If we crash between delete and add, data is lost — acceptable for a memory system vs. returning duplicates." This is a valid tradeoff.
| } | ||
|
|
||
| private key(text: string, task?: string): string { | ||
| const hash = createHash("sha256").update(`${task || ""}:${text}`).digest("hex").slice(0, 24); |
There was a problem hiding this comment.
SHA-256 truncated to 96 bits. More importantly, the cache key is sha256(task + text) but doesn't include the model name. If the embedding model changes at runtime (config hot-reload), cached vectors from the old model would be silently returned.
| const hash = createHash("sha256").update(`${task || ""}:${text}`).digest("hex").slice(0, 24); | |
| const hash = createHash("sha256").update(`${this._model}:${task || ""}:${text}`).digest("hex"); |
There was a problem hiding this comment.
Confirmed. Adding model name to cache key and using full SHA-256 digest.
|
|
||
| if (scopeFilter.length > 0) { | ||
| const scopeConditions = scopeFilter | ||
| .map((scope) => `scope = '${escapeSqlLiteral(scope)}'`) |
There was a problem hiding this comment.
💡 Medium: Missing OR scope IS NULL
Every other scope filter in this file includes OR scope IS NULL for backward compatibility with pre-scope data. bulkDelete does not. This means pre-scope memories can't be bulk-deleted by scope.
If intentional (safety: don't accidentally delete unmigrated data), add a comment. Otherwise:
| .map((scope) => `scope = '${escapeSqlLiteral(scope)}'`) | |
| conditions.push(`(${scopeConditions} OR scope IS NULL)`); |
There was a problem hiding this comment.
Confirmed. Adding OR scope IS NULL for consistency.
| const normalized = results.map((r) => { | ||
| const charLen = r.entry.text.length; | ||
| const ratio = charLen / anchor; | ||
| const logRatio = Math.log2(Math.max(ratio, 1)); |
There was a problem hiding this comment.
The JSDoc says short texts get a slight boost, but Math.max(ratio, 1) ensures logRatio is always ≥ 0, so the factor is always ≤ 1.0. Short texts get no boost, only long texts get penalized.
To match the docs, remove the clamp:
const logRatio = Math.log2(ratio); // allow negative for short textsOr update the docs to say "short texts are unaffected, long texts are penalized."
There was a problem hiding this comment.
Confirmed. Updating docs to match code behavior: short texts are unaffected, long texts are penalized. The Math.max(ratio, 1) is actually reasonable — boosting short texts could over-reward fragments.
| * and write them back via store.update() after recording access. | ||
| */ | ||
| export class AccessTracker { | ||
| private accessLog = new Map<string, { count: number; lastAt: number }>(); |
There was a problem hiding this comment.
💡 Medium: Unbounded growth
This Map grows monotonically — entries are added but never evicted. For long-running agents with thousands of memories, this is a memory leak.
Consider an LRU eviction policy or a max-size with oldest-first eviction.
There was a problem hiding this comment.
Confirmed. Adding max-size eviction.
Dhebrank
left a comment
There was a problem hiding this comment.
Review: Request Changes
Impressive architecture — the modular store/embedder/retriever/chunker decomposition is clean, the fallback chains (cross-encoder → cosine rerank, BM25+vector → vector-only) are well-designed, and error handling is thorough.
Must fix:
-
Missing
openaidependency.memory-embedder.tsimportsimport OpenAI from "openai"butopenaiis not incontainer/agent-runner/package.json. Build or runtime crash on first use. -
Temp env-file cleanup race. Secrets are written to a temp file and deleted after 30 seconds via
setTimeout, but the timer starts inbuildContainerArgs()before the container is spawned. If there's any delay beforespawn(), the file could be deleted before Docker reads it. Move cleanup to the container'scloseevent handler. -
scopeparameter onmemory_deleteis accepted but not enforced. The tool description says "deletion fails if the memory belongs to a different scope" but the implementation has a TODO comment and no enforcement. Either enforce it or remove the parameter to avoid false trust.
Should fix:
-
No tests for 4,240 lines of new code. The retrieval pipeline has complex scoring logic (recency boost, importance weighting, MMR diversity, RRF fusion) that needs unit tests.
-
Unrelated Gmail mount change bundled in. The
~/.gmail-mcpdirectory mount inbuildVolumeMountsis not related to the memory upgrade and should be its own PR. -
resolveEmbeddingConfig()called on everygetStore()invocation. Should be resolved once and cached alongside the singleton. -
stats()caps at 10K rows butcountRows()gives the true total. Category/scope breakdowns will be incomplete above 10K memories. -
Duplicated
clampInt()function in bothmemory-store.tsandmemory-retriever.ts.
The foundation is solid — address the blockers and this is a strong contribution.
e0d243f to
45b558f
Compare
|
@Dhebrank Thanks for the thorough review! All 8 items addressed and rebased to a single commit on main: Must fix:
Should fix:
|
45b558f to
2e1d965
Compare
Adds persistent vector-based memory to container agents using LanceDB. Hybrid retrieval pipeline combines vector search + BM25 full-text search with cross-encoder reranking, recency boost, importance weighting, time decay, length normalization, and MMR diversity filtering. Components: - memory-store: LanceDB storage with multi-scope support - memory-embedder: OpenAI-compatible embedding with key rotation, chunking - memory-retriever: hybrid retrieval with score fusion and reranking - memory-chunker: smart document chunking for long content - memory-access-tracker: reinforcement-based time decay - memory-utils: shared utilities (clampInt) - 83 unit tests covering scoring logic and retrieval pipeline Agents get 4 MCP tools: memory_store, memory_search, memory_delete, memory_count. Supports Gemini, Jina, OpenAI, Ollama embeddings and Jina, SiliconFlow, Voyage, Pinecone, vLLM rerankers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2e1d965 to
eb15980
Compare
Regression from PR qwibitai#1043 — memoryStore() passed empty {} as metadata instead of building l0_abstract, l1_overview, l2_content, confidence, valid_from fields like the original memory-lancedb-pro buildSmartMetadata(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a477a09 to
300f9e6
Compare
…-runner Phase 1 — Pure-logic modules: - smart-metadata.ts: SmartMemoryMetadata type, buildSmartMetadata(), temporal versioning (fact_key, supersedes/superseded_by), relations, support slices - memory-categories.ts: 6-category system (profile, preferences, entities, events, cases, patterns) with legacy aliases - decay-engine.ts: Weibull stretched-exponential decay, tier-specific beta/floors, importance-modulated half-life - tier-manager.ts: 3-tier promotion/demotion (peripheral→working→core) based on access patterns - adaptive-retrieval.ts: shouldSkipRetrieval() for trivial queries Phase 2 — Store + retriever upgrades: - memory-store.ts: patchMetadata(), lexicalFallbackSearch() - memory-retriever.ts: DecayEngine, TierManager, lifecycle boost, excludeInactive, adaptive skip - memory-noise-filter.ts: scoreContentQuality(), enhanced heuristics Phase 3 — LLM-powered features: - llm-client.ts: OpenAI-compatible JSON completion (gemini/openai/ollama/custom) - extraction-prompts.ts: extraction, dedup, merge prompts - smart-extractor.ts: LLM extract → vector dedup → LLM dedup → persist (6 action handlers) - self-improvement-files.ts: .learnings/LEARNINGS.md + ERRORS.md - ipc-mcp-stdio.ts: 5 new MCP tools (memory_list, memory_status, memory_update, memory_extract, self_improvement_log) - container-runner.ts: forward EXTRACTION_PROVIDER/API_KEY/MODEL/BASE_URL Phase 4 — Reflection system: - reflection-metadata.ts, reflection-slices.ts, reflection-ranking.ts, reflection-mapped-metadata.ts - reflection-item-store.ts, reflection-event-store.ts, reflection-store.ts, reflection-retry.ts - index.ts: wire extraction + reflection into PreCompact hook No LanceDB schema changes — all new fields in existing metadata JSON column. Category backward compat via normalizeCategory() + toLegacyCategory(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
300f9e6 to
1fe591d
Compare
|
@Dhebrank All advanced memory-lancedb-pro features have been ported. Here's what's new on top of the original PR: Phase 1 — Pure-logic modules:
Phase 2 — Store + retriever upgrades:
Phase 3 — LLM-powered features:
Phase 4 — Reflection system:
No breaking changes: No LanceDB schema changes (all new fields in existing metadata JSON column). Category backward compat via |
16 tests covering the full tier lifecycle: - peripheral→working promotion thresholds (access≥3, composite≥0.4) - working→core promotion thresholds (access≥10, composite≥0.7, importance≥0.8) - boundary conditions (just below each threshold) - core stability and demotion after 90d neglect - working→peripheral demotion after 30d neglect - full lifecycle simulation: peripheral→working→core - batch evaluation filtering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Why
The basic
memory.tsonly does vector (cosine) search. Keyword-heavy queries miss relevant memories, and there is no reranking, recency weighting, or noise filtering.What this adds
Replaces
memory.tswith memory-lancedb-pro (MIT):Embedding providers
Configured via
EMBEDDING_PROVIDERenv var (default:gemini):gemini(default)jinaopenaiollamacustomEMBEDDING_MODEL)EMBEDDING_BASE_URL)EMBEDDING_DIM)All overridable via
EMBEDDING_API_KEY,EMBEDDING_MODEL,EMBEDDING_BASE_URL,EMBEDDING_DIM.Rerank providers
Configured via
RERANK_PROVIDERenv var (default: disabled):jinasiliconflowvoyagepineconevllmnoneAll overridable via
RERANK_API_KEY,RERANK_MODEL,RERANK_ENDPOINT.New files
memory-store.tsmemory-retriever.tsmemory-embedder.tsmemory-chunker.tsmemory-access-tracker.tsmemory-noise-filter.tsmemory-query-expander.tsPublic API
Unchanged —
memoryStore,memorySearch,memoryDelete,memoryCountkeep the same signatures. Drop-in replacement.Build fixes
--legacy-peer-depsin Dockerfile:openai@4peer-wantszod@3, butclaude-agent-sdkrequireszod@4(optional peer dep, safe to skip)providerfieldcontainer-runner.tsforwards all embedding/rerank env vars to containersTest plan
npm run build+npx tsc --noEmitcompile cleanly (host + container)memoryStore/memorySearch/memoryDelete/memoryCountwork end-to-end🤖 Generated with Claude Code