npx skills add ...
npx skills add helixdb/skills --skill helix-memory-system
Design and operate an advanced AI agent memory system with the published HelixDB v3 SDKs using hybrid graph plus exact traversal-prefiltered vector and BM25 search. Use for long-term memory, profiles, RAG, recall, extraction, deduplication, consolidation, versioning, forgetting, categorisation, or ingestion. TypeScript-first (`@helix-db/helix-db@3.0.4`); a Rust v3 variant is in EXAMPLES.rust.md.
npx skills add helixdb/skills --skill helix-memory-system
Build a durable, per-tenant agent memory platform on Helix that combines graph relationships, vector similarity, and BM25 full-text in one database. This skill covers the whole memory lifecycle: raw context ingestion, extraction, memory generation, deduplication, updating/versioning, deletion/forgetting, categorisation, profile maintenance, and hybrid retrieval.
Helix is the storage and retrieval engine. A complete memory product also needs application workers for extraction, chunking, embeddings, relationship classification, reranking, connector sync, and profile summarisation.
Use this skill when the task is to:
Do not use this skill for generic query syntax questions. For builder/method details defer to helix-query-typescript (the default DSL), helix-query-rust, or helix-query-json-dynamic. This skill assumes those and focuses on the memory architecture on top of Helix.
@helix-db/helix-db@3.0.4) so the app can keep query generation near service code. Use EXAMPLES.rust.md only if the runtime is Rust or the team explicitly ships Rust queries.tenant_id because tenant-partitioned Helix text indexes currently require that name. Attach tenant_id to every tenant-owned node and edge.tenant_id partitions indexes while userId, containerId, projectId, or an app ACL decides which memories can be recalled. Default examples use userId as the second-level scope.text-embedding-3-small for production and benchmarkable memory systems: 1536 dimensions, stored as F32 arrays. The application computes embeddings client-side and passes numeric arrays (param.array(param.f32())). Helix does not embed text on the dynamic-query path; there is no Embed()/SearchV in the current DSL. Keep embedding model and dimension fixed for each vector index. Deterministic hash embeddings are acceptable only for local UI demos or smoke tests, not for quality benchmarks.Core labels: Tenant, User, UserProfile, SourceDocument, Chunk, Memory, Category, Entity, Session, optional Connector and IngestionJob.
Core edges: OWNS (Tenant/User→Memory), HAS_PROFILE (User→UserProfile), HAS_CHUNK (SourceDocument→Chunk), EXTRACTED_FROM (Memory→Chunk or SourceDocument), IN_CATEGORY (Memory→Category), MENTIONS (Memory→Entity), UPDATES (new Memory→old Memory), EXTENDS (Memory→Memory enrichment), DERIVES (inferred Memory→supporting Memory), RELATES_TO (Memory→Memory association), DERIVED_FROM (Memory→Session), optional PARENT_OF (Category→Category).
Fast and safe fields:
tenant_id on every tenant-owned node and edge, with equality indexes where used as an anchoruserId or an equivalent scope key on user/container-specific memories, source documents, and chunks; only intentionally shared records should be tenant-widememoryId, documentId, chunkId, categoryKey, entityKey, sessionId, and profileIdMemory.isLatest, validFrom, validTo, expiresAt, and deletedAt for record lifecycle filteringobservedAt, eventStartAt, eventEndAt, temporalText, and timezone when the memory is about a dated event or factMemory.embedding/Memory.content and optionally Chunk.embedding/Chunk.content, all partitioned by tenant_idFull spec, types, and index bootstrap are in REFERENCE.md.
Pick the mechanism by the question you are answering, and combine them deliberately:
| Need | Use | Why |
|---|---|---|
Tenant isolation (tenant_id), exact identity, lifecycle flags (deletedAt, expiresAt, validTo, isLatest), ordering/filtering (createdAt, salience) | Properties + equality/range index | Narrow anchors and safe filters. Tenant scope is non-negotiable. |
| Categorisation, entities, provenance, profile ownership, updates/extensions/derivations, association clusters, taxonomy | Graph edges | These are relationships; traverse and aggregate over them. |
| Deduplication, paraphrase recall, memories like this, chunks like this | Vector search | Semantic similarity; tolerant of rewording. |
| Exact names, ids, rare tokens, commands, file paths, product terms | BM25 text search | Embeddings blur exact tokens; BM25 preserves them. |
| Broad user context the model should always know | UserProfile node + summariser worker | Avoid multiple searches for stable identity/preferences/recent focus. |
| Raw documents and citations | SourceDocument + Chunk nodes | Memory facts are not a replacement for source-grounded RAG. |
Rule of thumb: never collapse a memory system to vector-only. Vectors miss exact names and have no notion of ownership, recency, contradiction, provenance, profile state, or category.
Always scope vector/BM25 searches with tenantValue = tenant_id. Tenant scope is necessary but not always sufficient: default user-memory recall must also filter by userId or the app's equivalent container/ACL unless the record is explicitly shared tenant-wide. Every recall path must filter out forgotten/stale records: deletedAt IsNull, isLatest = true, validTo IsNull, and expiresAt absent or in the future. Build those candidates before traversal-scoped vector or BM25 search so semantic and keyword recall both enforce exact visibility membership. If a route cannot express the full visibility rule, enforce the remainder in application code.
Helix gives you graph + search primitives. A full intelligent memory system also needs:
| Layer | Responsibility |
|---|---|
| Ingestion API | Accept text, chats, files, URLs, connector events, and direct memory writes. |
| Extractors | Convert PDFs, docs, HTML, images/OCR, audio/video transcripts, code, and structured data into text. |
| Chunkers | Split raw context by semantic sections, message turns, document headings, code AST boundaries, or transcript segments. |
| Embedding worker | Generate text-embedding-3-small 1536-dim F32 embeddings for memories and chunks before writing to Helix, unless the app has explicitly standardised on another model. |
| Memory generator | Extract atomic, entity-centric candidate facts from conversations/documents using the current turn plus recent context, active entities, recalled memories, and current date. |
| Relationship classifier | Decide whether each candidate UPDATES, EXTENDS, DERIVES, duplicates, or stands alone. |
| Profile summariser | Maintain UserProfile.staticSummary and dynamicSummary from latest memories. |
| Forgetting jobs | Run expiry, decay, stale-profile, and connector deletion sweeps. |
| Retrieval service | Rewrite queries, run vector + BM25 over memories/chunks, fuse, rerank, graph-expand, and pack context with citations. |
| Evaluation | Measure recall quality, stale-memory suppression, tenant isolation, latency, and token efficiency. |
Do not imply Helix automatically does extraction, chunking, embedding, relationship classification, profile generation, connector sync, reranking, or TTL. Those are application responsibilities unless the user has a managed service that provides them.
Each step links to complete examples in EXAMPLES.md (TypeScript) and EXAMPLES.rust.md (Rust).
SourceDocument, conversation/session, direct memory write, or connector update.text-embedding-3-small by default. Store/pass a 1536-length F32 vector.fact, preference, episode, procedure, or app-specific equivalents.Memory with tenant_id, memoryId, content, embedding, kind, salience, isLatest: true, and lifecycle timestamps; link ownership and provenance edges.Do not extract from the latest user message in isolation. The extraction worker should receive:
tenant_id plus userId, containerId, projectId, or ACL context)Resolve pronouns, ellipsis, and short follow-up answers before deciding whether to store a memory. If the assistant asks a memory-bearing follow-up question and the user answers briefly, convert the answer into a self-contained memory.
Extractor output should be structured enough for deterministic writes: shouldStore, self-contained content, kind, confidence, salience, entities, source pointers, scope, optional temporal fields, and a relationship decision (new, duplicate, EXTENDS, UPDATES, or DERIVES). Do not let a single vector-distance threshold decide updates; retrieve candidates with vector + BM25 and adjudicate exact duplicate vs update vs extension in application code.
Example:
accessCount, lastAccessedAt, and bounded salience.new -UPDATES-> old, set old isLatest = false and validTo, and optionally set deletedAt if it should disappear from normal recall.new -EXTENDS-> existing when the new fact enriches but does not replace the old fact.DERIVES edges to supporting memories and mark them as inferred with confidence metadata.content changes, re-embed and update embedding in the same write. Content and vector must never drift.validFrom, validTo, deletedAt) separate from real-world event time (eventStartAt, eventEndAt, temporalText). Updating a memory because a fact changed should invalidate the old record even if both facts refer to future or past dates..shouldAwaitDurability(true) in TypeScript, .should_await_durability(true) in Rust, helix.AwaitDurability(true) in Go). Recognize a real conflict from the stable transaction_conflict code (with HTTP 409 for remote calls), never by parsing the diagnostic message. Reload authoritative memory state before rebuilding the mutation, then replay with bounded backoff only when it is idempotent or otherwise safe. See the language skill and ../../docs/error-handling.md.Helix has no native TTL or decay. Forgetting is explicit write queries the app runs.
deletedAt = Expr.datetime() and filter it from reads. Reversible and audit-friendly.isLatest = false and validTo = Expr.datetime() when a memory is superseded.expiresAt < now.drop() only when policy requires physical deletion. drop() removes the node and incident edges; use dropEdgeById for surgical edge cleanup on multigraph-sensitive paths.Category nodes scoped by tenant_id and a unique categoryKey such as ${tenant_id}:${normalisedName}.Entity nodes scoped by tenant_id and a unique entityKey such as ${tenant_id}:${normalisedName}.UserProfile per user/container with profileId, tenant_id, userId, staticSummary, dynamicSummary, and updatedAt.Run multiple recall paths and fuse app-side:
UserProfile for always-on context.Memory nodes, tenant-scoped, user/container-scoped, and freshness-filtered.Chunk nodes for source-grounded RAG and citations, with the same owner/scope policy unless documents are intentionally shared.MENTIONS, IN_CATEGORY, EXTENDS, UPDATES, and RELATES_TO, bounded by depth and tenant filters.Do not:
.hx dialect (Embed(), SearchV, SearchBM25, AddV) for new dynamic/TS/Rust DSL workuserId as the text-index tenant property; use tenant_id for tenant-partitioned text/vector indexestenant_id alone is a safe recall boundary for org/team tenants; filter by userId, containerId, project ACLs, or an explicit shared-memory flagtenant_id only to Memory; every tenant-owned node and edge needs itmemoryId without also checking tenant_iddeletedAtvalidTo, deletedAt) with real-world event dates; use separate temporal fields for memories about trips, deadlines, appointments, or historical factstext-embedding-3-small unless the app has a better standard model$distance or $score after an out/in/both step; project rank metadata immediately after searchBatchCondition; it can only test variable emptiness/sizecontent without re-embeddingembedding arrays in API responses unless explicitly requiredCategory or Entity global by display name in a multi-tenant memory appBefore finishing:
readBatch() vs writeBatch() is correcttenant_idtenant_property = "tenant_id", and searches pass tenantValue = tenant_idtenant_id, user/container visibility, deletedAt IsNull, current/latest state, and expiry validitytenant_idcategoryKey, entityKey, etc.)observedAt, eventStartAt, eventEndAt, temporalText, or app equivalentsopenai:text-embedding-3-small and every vector is 1536-dim F32, unless the app explicitly standardises on another fixed model/dimensionExpr.datetime() and param.dateTime()embedding unless explicitly requirednull, collection/fold/mutation empties
remain [], and scalar 0 or false remains scalarREFERENCE.md — full data-model spec, tenant rules, indexes, modality cheat-sheet, embedding guidance, fusion/re-ranking formula, and TypeScript ↔ Rust API mapping.EXAMPLES.md — lifecycle scenarios as @helix-db/helix-db@3.0.4 TypeScript snippets. Default.EXAMPLES.rust.md — the same scenarios in the Rust DSL.helix-query-typescript, helix-query-rust, helix-query-json-dynamic, helix-query-optimize.