Rust port of claude-mem for OpenCode.
Full feature parity with claude-mem (TypeScript).
Upstream: https://github.com/thedotmack/claude-mem
Last reviewed commit: eea4f599c0c54eb8d7dcc0d81a9364f2302fd1e6
| Component | Status | Details |
|---|---|---|
| MCP Tools | ✅ 18 tools | search, timeline, get_observations, memory_, knowledge_, infinite_*, save_memory |
| Database | ✅ | PostgreSQL only (pgvector + tsvector/GIN), direct PgStorage (no dispatch enum) |
| CLI | ✅ 100% | serve, mcp, search, stats, projects, recent, get, hook (context, session-init, observe, summarize) |
| HTTP API | ✅ 100% | 65 endpoints (upstream has 56) |
| Storage | ✅ 100% | Core tables, session_summaries, pending queue |
| AI Agent | ✅ 100% | compress_to_observation(), generate_session_summary() |
| Web Viewer | ✅ 100% | Dark theme UI, SSE real-time updates |
| Privacy Tags | ✅ 100% | <private> content filtering applied in all paths (save_memory, compress_and_save, store_infinite_memory) |
| Pending Queue | ✅ 100% | Crash recovery, visibility timeout, dead letter queue |
| Hook System | ✅ 100% | CLI hooks: context, session-init, observe, summarize |
| Hybrid Capture | ✅ 100% | Per-turn observation via session.idle + debounce + batch endpoint |
| Project Exclusion | ✅ 100% | OPENCODE_MEM_EXCLUDED_PROJECTS env var, glob patterns, ~ expansion |
| Save Memory | ✅ 100% | Direct observation storage (MCP + HTTP), bypasses LLM compression |
| Circuit Breaker | ✅ 100% | Graceful degradation when PostgreSQL unavailable — MCP tools return empty results, HTTP returns 200 + X-Memory-Degraded header, auto-recovery on reconnect |
| Memory Quality | ✅ | Cross-project dedup, metadata enrichment, knowledge extraction for all types, usage tracking, trigram similarity dedup for knowledge |
| Structured Summaries | ✅ 100% | response_format: json_object, 9 typed fields (request, investigated, completed, next_steps, files_read, files_modified, decisions, discoveries) |
| # | Feature | Priority | Effort |
|---|---|---|---|
| 1 | Cursor/IDE hooks — IDE integration | LOW | Medium |
| Feature | Status | Notes |
|---|---|---|
| Infinite Memory | ✅ Ready | PostgreSQL + pgvector backend for long-term AGI memory. Session isolation, hierarchical summaries (5min→hour→day), content truncation. Enabled via INFINITE_MEMORY_URL. Raw events are NEVER deleted — drill-down API allows zooming from any summary back to original events. |
| Dynamic Memory | ✅ Ready | Solves "static summaries" problem. Deep Zoom: 4 HTTP endpoints (/api/infinite/expand_summary/:id, /time_range, /drill_hour/:id, /drill_minute/:id) for drilling down from summaries to raw events. Structured Metadata: SummaryEntities (files, functions, libraries, errors, decisions) extracted via response_format: json_object. Enables fact-based search even when text summary is vague. |
| Semantic Search | ✅ Ready | fastembed-rs (BGE-M3, 1024d, 100+ languages). PostgreSQL: pgvector. Hybrid search: FTS BM25 (50%) + vector similarity (50%). HTTP endpoint: /semantic-search?q=.... |
- repo: thedotmack/claude-mem
- watch: src/services/, src/constants/
- ignore: .test.ts, README, docs/
- last_reviewed: eea4f599c0c54eb8d7dcc0d81a9364f2302fd1e6
crates/
├── core/ # Domain types (Observation, Session, etc.)
├── storage/ # PostgreSQL + pgvector + migrations + circuit breaker (circuit_breaker.rs: 3-state Closed/Open/HalfOpen, exponential backoff 30s-300s)
├── embeddings/ # Vector embeddings (fastembed BGE-M3, 1024d, multilingual)
├── search/ # Hybrid search (FTS + keyword + semantic)
├── llm/ # LLM compression (Antigravity API)
├── service/ # Business logic layer (ObservationService, SessionService, QueueService)
├── http/ # HTTP API (Axum)
├── mcp/ # MCP server (stdio)
├── infinite-memory/ # PostgreSQL + pgvector backend
└── cli/ # CLI binary
crates/storage/src/pg_storage/— modular PG storage (mod.rs + domain modules)crates/storage/src/pg_migrations.rs— PostgreSQL schema migrationscrates/mcp/src/— MCP server: lib.rs + tools.rs + handlers/crates/http/src/handlers/— HTTP handlers (11 modules)crates/core/src/observation/low_value_filter.rs— configurable noise filter (SPOT), env:OPENCODE_MEM_FILTER_PATTERNScrates/llm/src/— AI agent: client, observation, summary, knowledge, insights
Status: ✅ Fully Implemented
LLM always creates NEW observations even when near-identical ones exist. The existing_titles dedup hint in the prompt only lists titles — the LLM lacks enough context to confidently mark duplicates as negligible. Post-facto dedup (cosine similarity merge, background sweep) catches some duplicates but is fundamentally reactive: duplicates are created, then cleaned.
- API calls are free (zero cost concern)
- ~100 observations in DB, growing slowly
- FTS + GIN index exists on
search_vectsvector column response_format: json_objectis required for all LLM calls- Raw events preserved in Infinite Memory (observations are derived views)
One code path, three outcomes. Before LLM compression, retrieve top 5 candidate observations via FTS on raw input text. Feed their full content to the LLM alongside the new tool interaction. LLM returns a discriminated result:
- CREATE: Genuinely new knowledge → full observation (current behavior)
- UPDATE(target_id): Refines existing observation → full replacement of target's content fields
- SKIP: Zero new information → log and discard
-
Merge-or-create gate (separate pre-search → conditional prompt branching) — Rejected. Two code paths, two prompt templates, patch parsing, false positive recovery. Over-engineered for a context starvation problem.
-
Aggressive post-facto dedup only (lower thresholds, more frequent sweeps) — Rejected. Treats symptoms. Duplicates still created, wasting LLM calls and storage churn.
-
Pre-embed raw input for semantic search — Rejected. Raw tool output has low similarity to compressed observations (different vocabulary, length, structure). FTS keyword matching is sufficient for candidate retrieval.
Phase 1: Candidate retrieval (in ObservationService, before LLM call)
- Extract keywords from raw input via
plainto_tsquery - Query
observationsvia existingsearch_vecGIN index, limit 5 - Also include 2-3 most recent observations from same session (most likely merge targets)
Phase 2: Prompt modification (crates/llm/src/observation.rs)
- Replace
existing_titleswith full candidate observations (id + title + narrative + facts) - Add DECISION section: CREATE / UPDATE(id) / SKIP
- Response schema becomes discriminated union with
actionfield
Phase 3: Response handling (crates/service/src/observation_service/)
- Parse
actionfield from LLM response - CREATE → existing
persist_and_notifypath - UPDATE → validate target_id exists and was in candidate set → full field replacement → regenerate embedding
- SKIP → log at debug, return early
Phase 4: Simplify dedup layers
- Post-facto cosine dedup becomes safety net only (keep threshold at 0.85)
- Background sweep remains as defense-in-depth (30 min interval)
- Echo detection (0.80) stays unchanged (different purpose: injection recursion prevention)
- Candidate retrieval quality: if FTS misses the relevant observation, LLM creates a duplicate. Mitigate by also including recent same-session observations.
- LLM hallucinating target_id: validate returned ID exists AND was in candidate set. If not → treat as CREATE.
- Token growth: 5 candidates × ~500 tokens = ~2500 extra tokens per call. Negligible for current models.
- Gate MCP review tooling intermittently returns 429/502 or "Max retries exceeded", blocking automated code review.
- Test coverage: ~40% of critical paths. Service layer, HTTP handlers, infinite-memory, CLI still have zero tests.
- CUDA GPU acceleration blocked on Pascal — ort-sys 2.0.0-rc.11 pre-built CUDA provider includes only SM 7.0+ (Volta+). GTX 1060 (SM 6.1 Pascal) gets
cudaErrorSymbolNotFoundat inference. CUDA EP registers successfully but all inference ops fail. CUDA 12 compat libs cleaned up from home-server. Workaround: CPU-only embeddings withOPENCODE_MEM_DISABLE_EMBEDDINGS=1for throttling. To resolve: either build ONNX Runtime from source withCMAKE_CUDA_ARCHITECTURES=61, or upgrade to Volta+ GPU.
Permissive CORS on HTTP server— fixed by removing CorsLayerLocal HTTP server fails to start if port 37777 is already in use— fixed by returning a clear error message with a shutdown command, and removing SO_REUSEPORT to prevent load balancing with zombie instances.— fixed by moving env var setting to CLIstd::env::set_varwill become unsafe in edition 2024main()before tokio runtime init and wrapping in unsafe.Infinite Memory drops filtered observations— fixed by callingstore_infinite_memoryconcurrently withcompress_and_savepg_migrations non-transactional— fixedpg_migrations .ok() swallows index errors— fixedKnowledgeType SPOT violation— fixedstrip_markdown_json duplicated— fixedsave_memory bypasses Infinite Memory— fixedadmin_restart uses exit(0)— fixedInfinite Memory migrations not evolutionary— fixedInfinite Memory double connection pool— fixedsearch_by_file missing GIN index— fixedparse_pg_vector_text string parsing overhead— fixed by using pgvector crateHybrid search stop-words fallback— fixedsqlx missing uuid feature— fixedsqlx missing TLS feature— fixedInconsistent HTTP error responses— fixedPagination limit inconsistency— fixedsave_memory ambiguous 422— fixedCLI hook.rs swallows JSON parse errors— fixedimport_insights.rs regex truncation— fixedimport_insights.rs title_exists swallows DB errors— fixedsession_service hardcoded opencode binary— fixedsession_service inconsistent empty-session handling— fixedbuild_tsquery empty string crash— fixedparse_json_value unnecessary clone— fixedget_all_pending_messages unfiltered— fixedget_queue_stats dead 'processed' count— fixedInfinite memory compression pipeline starvation—run_full_compressionnow queries per-session viaget_sessions_with_unaggregated_*+get_unaggregated_*_for_session, eliminating fixed cross-session batch that caused threshold starvationCode Duplication in observation_service.rs— extracted sharedpersist_and_notifymethodBlocking I/O in observation_service.rs— embedding calls wrapped inspawn_blockingData Loss on Update in knowledge.rs— implemented provenance merging logicSQLITE_LOCKED in knowledge.rs— SQLite backend removedHardcoded filter patterns— extracted tolow_value_filter.rswithOPENCODE_MEM_FILTER_PATTERNSenv supportPre-commit hooks fail on LLM integration tests— marked with#[ignore], run explicitly viacargo test -- --ignoredSilent data loss in embedding storage— atomic DELETE+INSERT via transactionPG/SQLite dedup divergence— SQLite backend removedSQLite crash durability— SQLite backend removedSilent data fabrication in type parsing— 18 enum parsers now log warnings on invalid valuesSilent error swallowing in service layer— knowledge extraction, infinite memory errors at warn levelLLM empty summary stored silently— now returns error, prevents hierarchy corruptionEnv var parse failures invisible—env_parse_with_defaulthelper logs warningsMCP handlers accept empty required fields— now reject with errorMCP stdout silent write failures— error paths now break cleanlyUnbounded query limits (DoS)— SearchQuery/Timeline/Pagination capped at 1000, BatchRequest.ids at 500pg_storage.rs monolith (1826 lines)— split into modular directory: mod.rs + 9 domain modules4-way SPOT violation in save+embed— centralized through ObservationService::save_observation()Blocking async in embedding calls— wrapped in spawn_blocking~70 unsafe— replaced with TryFrom/checked conversions (3 intentional casts with #[allow(reason)])ascasts in pg_storagesqlite_async.rs boilerplate (62 self.clone())— SQLite backend removed entirelyZero-vector embedding corruption— guard in store_embedding + find_similarStale embedding after dedup merge— re-generate from merged contentmerge_into_existing not transactional (SQLite)— SQLite backend removedNullable project crash in session summary (PG)— handle Option<Option> correctlyInfinite Memory missing schema initialization— addedmigrations.rswith auto-run inInfiniteMemory::new()Privacy leak: tool inputs stored unfiltered in infinite memory—filter_private_contentapplied to inputs before storageSPOT: ObservationType/NoiseLevel parsing duplicated 10+ times— extractedparse_pg_observation_type()andparse_pg_noise_level()helpersSPOT: hybrid_search_v2 inline row parsing copy-pasted from row_to_search_result— usesrow_to_search_result_with_scorenowSPOT: map_search_result + map_search_result_default_score duplicate— merged into single function withscore_col: Option<usize>rebuild_embeddings false claim about automatic re-embedding— now states to run backfill CLI commandInconsistent default query limits (20/50/10) across HTTP/MCP— unified viaDEFAULT_QUERY_LIMITin core/constants.rsMCP tool name unwrap_or("") silently accepts empty— explicit rejection with error messageScore fabrication: missing score defaulted to 1.0 (perfect)— changed to 0.0 (no match)MAX_BATCH_IDS duplicated between http and mcp— unified in core/constants.rs5x Regex::new().unwrap() in import_insights.rs— wrapped in LazyLock staticsLlmClient::new() panics on TLS init failure— returns Result, callers handle with ?Unsafe— replaced with TryFrom/checked conversionsascasts in pipeline.rs, pending.rsHTTP→Service layer violation— all 9 handlers migrated to useQueueService,SessionService,SearchService. Zero directstate.storage.*calls in handlers.dedup_tests.rs monolith (1050 lines)— split into 4 modules: union_dedup_tests, merge_tests, find_similar_tests, embedding_text_tests. 3 misplaced tests relocated.StoredEvent.event_type String— changed toEventTypeenum withFromStrparser. Unknown types logged as warning and skipped (Zero Fallback).No newtypes for prompt_number/discovery_tokens—PromptNumber(u32)andDiscoveryTokens(u32)newtypes in core, used in Observation, SessionSummary, UserPrompt.No typed error enums in leaf crates—CoreError(4 variants),EmbeddingError(4 variants),LlmError(7 variants withis_transient()) defined and used in public APIs.SQLite timeline query missing noise_level— SQLite backend removedPG search/hybrid_search empty-query fallback type mismatch— fallback returnedVec<Observation>whereVec<SearchResult>was expected. Wrapped withSearchResult::from_observation.PG knowledge usage_count INT4 vs i64 mismatch—global_knowledge.usage_countwasINT4in PG but decoded asi64. ALTERed column toBIGINT.Memory injection recursion— observe hook re-processed<memory-*>blocks injected by IDE plugin, creating duplicate observations that got re-injected in a loop. Addedfilter_injected_memory()at all entry points: HTTP observe/observe_batch (before queue), session observation endpoints, CLI hook observe, save_memory, plus service-layer defense-in-depth.Dedup threshold env var without bounds validation—OPENCODE_MEM_DEDUP_THRESHOLDandOPENCODE_MEM_INJECTION_DEDUP_THRESHOLDnow clamped to [0.0, 1.0] on parse. Values outside cosine similarity range no longer silently disable detection.NaN/Inf embedding validation—store_embeddingaccepts NaN/Infinity vectors. Addedcontains_non_finite()guard. Non-finite floats now rejected with error.SQLite PRAGMA synchronous mismatch— SQLite backend removedSearchService bypasses HybridSearch abstraction— fixed, correctly delegates to internal structQueue processor UUID entropy & format violation— fixed, uses strict UUIDv5 namespace over SHA-256 string inputObservationType SPOT violation— fixed, LLM prompt string dynamically generated from enum variantsQueueprocessor bottleneck— fixed, background tasks spawned as fire-and-forget to avoid head-of-line blockingSettings state ignored— fixed, API updates propagated toLlmClient::update_configandstd::env::set_varPrivacy leak in title— fixed, appliedfilter_private_contentinsidesave_memorytitle paramPrivacy filter omission in queues— fixed,observeandobserve_batchhandlers apply filter before database insertionInfinite Memory LLM request bypasses retry logic— already fixed (uses LlmClient::chat_completion with backoff)Knowledge extraction skips updated observations— already fixed (extracts fromsave_resultregardless of is_new flag)Dedup sweep overwrite bug— already fixed (compute_merge resolves by noise_level importance)Infinite memory time hierarchy violation— already fixed (pipeline buckets strictly by 300s window)Double-quoted observation_type/noise_level corruption (733/904 records)— fixed by DB migration (TRIM(BOTH '"')) +FromStrdefense-in-depth normalization (trim().trim_matches('"')) inObservationType,NoiseLevel,ConceptInfinite Memory call_id UNIQUE constraint breaks pipeline on 2nd event— fixed by using deterministic UUID (observation ID) as call_id instead ofString::new()Queue UUID hash omits tool_input — silent data loss on same-second calls— fixed by includingtool_inputin UUIDv5 hashsave_and_notify title collision retry exhaustion silently drops data— fixed: returnsErr(goes to DLQ) instead ofOkwhen all 5 retries failKnowledge extraction noise level inversion (skipped Critical/High instead of Low/Negligible)— fixed: now skipsLow | Negligible, extracts knowledge from high-value observationsget_events_by_time_range broken fallback query (ILIKE instead of time range)— fixed: correctWHERE ts >= $1 AND ts <= $2with proper parameter bindingget_session_by_content_id non-deterministic on duplicate content_session_id— fixed: addedORDER BY started_at DESC LIMIT 1Vector search string serialization overhead (10KB string per query)— fixed: usespgvector::Vectorbinary protocol in semantic and hybrid searchFTS split-on-punctuation in tsquery builders—build_tsqueryandbuild_or_tsquerynow split on non-alphanumeric chars instead of filtering, preventing fused tokens likesrcutilsrsGlobal observations invisible in project-scoped searches— allWHERE project = $1queries now includeOR project IS NULLSQL operator precedence fragility—project = $1 OR project IS NULLwrapped in parentheses in all queriesCJK single-char filter killed non-Latin search— removedchars().count() < 2filter, added DoS guard (100 term truncate)Infinite Memory data loss on LLM compression failure—store_infinite_memorynow runs concurrently withcompress_and_saveviatokio::join!SPOT violation in tsquery builders— extractedtokenize_tsqueryandbuild_joined_tsqueryshared helpersCircuit breaker never trips on DB connection failures—StorageError::is_transient()now coversPoolClosed,WorkerCrashed, and connection-refusedDatabaseerrors.is_unavailable()delegates tois_transient().ServiceError::is_db_unavailable()checksSearch(anyhow)andSystem(anyhow)via string inspection for connection patterns. Service layerwith_cb()records success/failure on every storage call. HTTP handlers useFrom<ServiceError> for ApiError(notanyhowwrapping) so theDegradedvariant is triggered correctly.Circuit breaker bypass in infinite memory MCP handlers— fixed by addingcb_fast_fail_infiniteguards to all infinite memory MCP tool handlersstrip_markdown_json fails on LLM preamble— fixed byfind/rfind-based extraction instead of regex, handles arbitrary preamble text before JSONparse_limit SPOT violation (hardcoded DEFAULT_QUERY_LIMIT)— fixed by accepting per-tool defaults, each MCP tool specifies its own default limitZero Fallback in parse_pg_observation_type/parse_pg_noise_level— fixed by returningDataCorruptionerrors instead of silently defaulting to fallback valuesResponseFormat.format_type raw String— fixed byResponseFormatTypeenum with typed variantsKnowledgeQuery/SaveKnowledgeRequest knowledge_type raw String— fixed by usingKnowledgeTypeenum directly in query/request typesRaw— fixed by checked conversions (ascasts in pipeline.rsTryFrom,try_into)Files >300 lines (memory.rs 488, search_service.rs 471, pg_storage/mod.rs 470, observation_service/mod.rs 459)— fixed by module splits into focused submodulesInfinite Memory spin-loop—release_events()andrelease_summaries_*()now setprocessing_started_at = NOW()instead of NULL, providing cooldown via the existing 5-minute visibility timeoutdeduplicate_by_embedding O(N²) blocks async runtime— O(N²) comparison loop moved intospawn_blockingUnstable pagination in get_observations_paginated— addedidas secondaryORDER BYtie-breakerTombstone save silent discard—save_observation(&tombstone)errors now logged at warn level with contextCross-project dedup failure (5x duplicate noise-classification observations)—find_candidate_observationsnow searches across all projects instead of current-only, preventing cross-project knowledge duplicationsave_memory metadata poverty (empty facts/concepts/keywords)— background LLM enrichment extracts structured metadata after persist, with re-fetch from DB before knowledge extractionKnowledge extraction gate too restrictive (Gotcha-only)— removed observation_type filter, all non-Low/Negligible observations now eligible for LLM-driven knowledge extractionMCP knowledge usage_count always zero— increment moved from MCP handlers to KnowledgeService (SPOT), both MCP and HTTP paths now track usageMulti-byte truncation OOM in LLM error messages— replacedresponse.get(..300).unwrap_or(&response)withcore::truncate()char-boundary-safe helperPanic in spawned tokio tasks aborts server— removed.expect()and.unwrap()from queue_processor, queue, serve spawned tasksEnrichment clobbers concurrent observation updates—update_observation_metadatachecksrows_affected(), logs and skips on concurrent modificationsave_memory enrichment silently failing (all 53 manual observations had empty metadata)— root cause:OPENCODE_MEM_API_URLmissing from both systemd service and opencode MCP config, causing LLM calls to go toapi.openai.comwith an Antigravity API key (silent auth failure). Fixed by addingOPENCODE_MEM_API_URL=https://antigravity.quantumind.ruto both configs. Addedbackfill-metadataCLI command for re-enrichment.MCP binary split-brain (opencode-memory SQLite vs opencode-mem PostgreSQL)— fixed by unifying MCP config to use opencode-mem with explicit DATABASE_URL, INFINITE_MEMORY_URL env vars in opencode.jsonMissing OPENCODE_MEM_API_URL in systemd and MCP config— fixed by adding https://antigravity.quantumind.ru to both configs, enabling LLM enrichmentOutdated model in systemd (gemini-3-pro-high)— fixed: updated to gemini-3.1-pro-highobservation_type search filter case-sensitive— fixed: lowercased at service layer in hybrid_ops.rsCLI search bypasses SearchService— fixed: uses smart_search() for semantic/hybrid routingbackfill-metadata single-batch truncation— fixed: proper loop with progress tracking and infinite-loop preventionSession summaries never generated (0/2168 sessions)—get_sessions_without_summariesjoined onsessions.id(UUID) but observations store IDE content session IDs (ses_*) that never match. Fixed: query now groups observations bysession_iddirectly, bypassing the sessions table.generate_pending_summariesusessave_summaryinstead ofupdate_session_status_with_summary.Infinite Memory migration 20260314000000 references— fixed table name in normalize_project_names migrationeventsinstead ofraw_eventsSemantic search poor relevance (scores 0.48-0.55)— root cause: IVFFlat index with lists=100 on 959 vectors, probes=1 searched only 1% of vector space. Fixed by replacing with HNSW(m=16, ef_construction=64). Scores improved to 0.55-0.94 with correct semantic ranking.Missing— added migration 20260315000003updated_atcolumn on observations tableKnowledge duplicates (4x Telegram MTProto entries)— cleaned up, kept entry with highest usage_count5 observations with empty metadata from manual import— backfilled via CLI— added alias alongside existing/api/semantic-searchroute inconsistency/semantic-searchBackground processor not started in MCP mode— added shared MaintenanceServices + run_maintenance_tick(), both HTTP and MCP use same schedulerAdmin endpoints CSRF via missing Json extractor— added Json(()) body to destructive admin endpointsAdmin auth bypass via loopback trust behind reverse proxy— dual-mode: token required if set, loopback-only if unsetInfinite memory compression poison pill on >200 events— pipeline fetches up to 10K events, chunks at 200 per LLM callstrip_markdown_json forward scan truncates JSON with embedded backticks— uses rfind for closing fenceMCP background loop SPOT violation (only ran compression)— extracted shared MaintenanceServices, all 7 tasks run in both modesPipeline dead code (chunking unreachable due to batch limit)— removed per-iteration batch limit, proper chunking for large bucketsrun_full_compression bypassed circuit breaker— routes through CB with should_allow/record_success/failureSession summaries unstructured free text— structured via response_format: json_object with 9 typed fieldsJSON corruption in infinite memory compression pipeline— serialized JSON string was truncated by.chars().take(N), breaking closing braces/quotes and poisoning LLM prompts. Fixed bytruncate_json_values()which truncates text fields inside theserde_json::Valuebefore serialization, preserving valid JSON structure— all replaced with.chars().take(N).collect()SPOT violation (5 call sites)opencode_mem_core::truncate()which handles char boundaries correctly. Affected:compression_prompt.rs,compression.rs,save_memory.rs,knowledge.rs,observations.rs,unified.rs