Skip to content

Changelog

All notable changes to Engrama will be documented here.

Format: Keep a Changelog Versioning: Semantic Versioning


[Unreleased]

Added

  • mcp.json server manifest for marketplace / public-build publication. It declares the server metadata, install commands, a Claude Desktop drop-in mcpServers block, the configurable environment variables, and the full catalog of all 14 MCP tools with their JSON-Schema inputSchema and tool annotations. The version is kept in lock-step with pyproject.toml.
  • examples/ fleshed out — a runnable Python SDK quickstart (examples/python_sdk/quickstart.py), a LangChain tool-wrapping example, an examples/README.md index, and a claude_desktop/config.dev.json variant for source checkouts.
  • README "Security considerations" section summarising data residency, Neo4j authentication, tenant isolation, GDPR erasure and network exposure.

[0.15.0] — 2026-06-26

Added

  • engrama_surface_insights accepts title / query filters. title returns one pending Insight by exact title; query returns pending Insights related to the query via the same cosine gate as the search hint — so the agent can surface exactly what a search flagged instead of dumping the backlog. The output now includes each Insight's engrama_id.

Changed

  • Archived nodes are excluded from search. fulltext_search (SQLite + Neo4j) now filters out status='archived' nodes, matching the graph-search and decay paths — a soft-deleted ("forgotten") node no longer resurfaces in search results. Archive is the trash; restoring/searching it is not exposed.

Fixed

  • The search proactive_hint for pending Insights now matches the real queue. It was computed from a separate fulltext search filtered to type=='Insight' — with no status filter — so it fired on approved, dismissed or synced Insights too, sending the agent to engrama_surface_insights for a queue that came back empty (false positive). It also projected title/body, which fulltext doesn't return, so the surfaced item was a contentless {score, confidence} stub. The hint now reads the same pending queue (status='pending', scoped) as engrama_surface_insights and gates each entry by pure cosine of the reused query embedding (≥ τ, ENGRAMA_INSIGHT_HINT_TAU, default 0.6) and confidence (≥ κ, ENGRAMA_INSIGHT_HINT_KAPPA, default 0.6). It short-circuits on an empty queue or a missing query vector (fulltext-only). The payload carries the matched engrama_id(s) so the agent can surface those specific Insights.

Security

  • MCP tools no longer return raw exception text to the client. Several error paths returned str(e) (and engrama_status's startup_error / backend / vault health errors), which could leak Neo4j URIs, filesystem paths and driver internals — the same class of disclosure as the fixed Lucene-error. A _safe_error helper now logs the detail server-side and returns a generic message; all client-facing error payloads are redacted.
  • User content is no longer written to logs. Node names/titles, search queries, relation endpoints and vault note paths were logged at INFO/WARNING; in a shared log sink that is tenant content exposure. Logs now carry only non-PII context (node label, relationship type) plus the exception.

[0.14.0] — 2026-06-25

Added

  • engrama_gdpr_forget tool — GDPR right-to-erasure (Spec 001 US-3). Permanently erases the caller's own memory: every node, relationship and embedding belonging to the resolved identity, plus its notes in Engrama's internal vault. Real erasure — no soft-delete, no server-side backup. Scope is always the caller (resolved from the request headers); a tenant can only erase itself, never another. mode='dry-run' (default, safe) reports the per-label counts that would be erased; mode='apply' performs the deletion and is idempotent. Export first if you want a copy.
  • engrama_status.admin_tools. New field listing the not-tenant-isolated tools (engrama_status, engrama_reindex) with a reason each, so a multi-tenant gateway can discover what to gate at runtime instead of hardcoding names.
  • ENGRAMA_REQUIRE_IDENTITY — fail-closed identity (defence-in-depth). When set, a request carrying no identity headers is rejected (ScopeUnresolved) instead of falling back to the standalone single-user identity. For a multi-tenant deployment fronted by a gateway that always injects X-Engrama-Org-Id/X-Engrama-User-Id, this turns a gateway misconfiguration or header-drop into a closed failure (no pooling of callers into one shared scope) rather than an open one. Unset (the default) preserves the bare-OSS standalone behaviour. The partial-identity case (exactly one header) is unconditionally fail-closed as before.

Changed

  • Hybrid search relevance base is now Reciprocal Rank Fusion, with a typed-graph node-distance rerank (spec 002, #98/#99). The default scoring changed from the min-max linear blend α × vector + (1-α) × fulltext + β × graph_boost + γ × temporal to rrf + β × graph_distance + γ × temporal:
  • RRF base — vector and fulltext are fused on rank only (Σ 1/(k + rank), k=60, normalised to [0,1]), making the relevance base scale-invariant: rescaling either channel can no longer flip the order.
  • Node-distance rerank — the degree-count graph_boost is replaced by a graph-distance signal (result-set cohesion + optional query-anchor proximity), computed only over the fused candidate window.
  • The engrama_search payload now exposes the per-result rrf_score and graph_distance_score. New tuning knobs (ENGRAMA_FUSION_MODE, ENGRAMA_RRF_K, ENGRAMA_GRAPH_RERANK, ENGRAMA_GRAPH_HOPS, ENGRAMA_COHESION_DECAY, ENGRAMA_ANCHOR_BOOST, ENGRAMA_ANCHOR_BETA, ENGRAMA_FANOUT_CAP). The legacy linear blend is preserved for a one-flag revert via ENGRAMA_RANKING_LEGACY=1.
  • Inline relations resolve by confidence, not silent stubbing (#93). When engrama_remember gets an inline relation whose target doesn't match an existing node exactly, measured name similarity (scope-filtered, so it never sees another tenant's nodes) now drives a three-way decision instead of always minting a stub:
  • near-certain in-scope match → connects to that node and reports relations_resolved (resolved_by: fuzzy_match) — auto-connection is always visible, never silent;
  • ambiguous candidates → creates nothing and returns relations_ambiguous with a did_you_mean list of in-scope names, so a wrong edge (harder to detect than a missing one) is never guessed;
  • no similar candidate → creates a stub as before, reported in relations_stubbed.

Thresholds are conservative named constants, tunable as the graph grows.

Security

  • engrama_context no longer discloses another tenant's node. The context/root-node read (get_node_with_neighbours) scope-filtered only the neighbours — the requested node itself was fetched by key with no scope check, so a resolved tenant could read another tenant's node (with all its enrichment fields) by guessing its (label, name). Both backends now require the root node to be visible at the caller's scope when one is supplied (cross-tenant or out-of-scope → "not found"); a None scope keeps the admin/debug fetch-by-key. Adds the node_visible scope predicate.
  • GDPR erasure now also purges the SQLite full-text index. gdpr_forget (SQLite) deleted nodes, edges and embeddings but left the subject's rows in the content-storing nodes_fts FTS5 table, so the erased subject's indexed text (name/title/description/notes/…) survived on disk after a "permanent" delete. The erasure now removes the matching nodes_fts rows too, matching every other hard-delete path. Neo4j was unaffected (DETACH DELETE is atomic).
  • Caller-supplied property keys are backtick-quoted before reaching Cypher. A node's property key names (free-form via the engrama_remember properties bag) were interpolated raw into SET n.<key> = $p0 in both the sync and async Neo4j stores. A malformed key (a backtick, space or =) could break the Cypher parser and crash the write — the same class of defect as the unescaped Lucene query. Property keys are now wrapped with escape_cypher_identifier (backtick-quoted, internal backticks doubled) so any key is matched literally and can never alter the query structure. Property values were already parameterised and unaffected.
  • CLI --label / --labels are validated against the schema whitelist. engrama decay and engrama migrate keys passed their label arguments straight to the backend, which interpolates them into Cypher — the MCP and engine paths already validated labels, but these CLI paths did not. decay/query_at_date/migrate keys now reject any label that is not a known NodeType before it can reach a query (EngramaEngine.decay_scores validates too, covering SDK callers).
  • merge_relation now scope-filters both endpoints (#93). Endpoint matching was unscoped while lookup_node_label was tenant-filtered. That asymmetry let a relation write reach a node owned by another tenant — forming a cross-tenant edge, and letting engrama_relate's success/failure act as an existence oracle for another tenant's node names. Both endpoints are now matched within the caller's scope (scope=None still allowed for admin/import/migration), and the MCP relate paths pass scope so the edge is stamped with identity. An endpoint owned by another tenant resolves to "not found".
  • engrama_reindex scan is now tenant-scoped. list_unembedded_nodes takes an optional scope; the reindex tool passes the request's resolved scope, so detect/classify only sample the caller's own vector-less nodes and apply only re-embeds them. Previously the scan was cross-tenant and exposed other tenants' node names/ids in its samples. The internal opportunistic sweep and the admin CLI keep the deployment-wide backfill via scope=None.

Fixed

  • engrama_search no longer crashes on Lucene syntax characters in the query. A query containing characters special to the Lucene classic parser — e.g. / in "CI/CD pipeline ..." — made the Neo4j fulltext index raise Neo.ClientError.Procedure.ProcedureCallFailed (TokenMgrError: the / was read as the start of an unterminated regex literal). The query string is now escaped (escape_lucene_query) before reaching db.index.fulltext.queryNodes in both the sync and async Neo4j stores, so every term is matched literally (CI/CD still tokenises into ci + cd). Boolean operators && / || can no longer form either.
  • Relation merge no longer drops edges to nodes that exist (#93). Endpoints are now matched the same permissive, case-insensitive COALESCE(name, title) way lookup_node_label resolves them, instead of a single statically re-derived key. Lookup and merge can no longer disagree: a node keyed by title while TITLE_KEYED_LABELS predicts name (or differing only in case) now connects instead of silently failing. The relations_failed (match_failed) report path remains as a backstop.
  • Silent inline-relation failures are surfaced (#93). Relations that don't connect are no longer hidden behind status: ok / relations_created: 0 with only a server-log warning — relations_failed lists targets where no edge was created, with an explanatory _note mirroring relations_rejected.
  • engrama_relate pinpoints the failing endpoint (#93). A failed relate now returns a structured status: "error" naming which endpoint was missing, and distinguishes a wrong/out-of-scope name from a label mismatch (endpoint exists under a different label) instead of a vague "could not find either".

Docs

  • Documented fail-closed multi-tenant isolation across security, architecture, and graph-schema (EN + ES); refreshed the README for the PyPI release (pip install engrama).

[0.13.0] — 2026-05-28

Breaking — pre-1.0 minor bump (semver 0.x). This release introduces fail-closed multi-tenant scoping. Per Constitution P9 it is a MAJOR-equivalent change; while the package is pre-1.0 that is expressed as a minor bump with migration notes (OQ-10, resolved 2026-05-28). 0.12.0 was the earlier non-breaking docs/latency release, so the breaking work takes 0.13.0. 1.0.0 is reserved for when the tenancy/sharing model is declared stable.

Breaking (Spec 001 — multi-tenant identity-scoped memory)

All reads and writes are now keyed by (org_id, user_id). A node or relation that doesn't carry both is invisible to every scope. Existing installations MUST run the migration command below before serving traffic on the new release — otherwise pre-existing data appears to have vanished. Detailed design lives under specs/001-tenant-scoped-memory/.

  • Fail-closed read filter. Every read path (engrama_search, engrama_context, engrama_reflect, engrama_surface_insights, engrama_approve_insight, engrama_write_insight_to_vault, the seven reflect detectors, both backends' count_labels / lookup_node_label / Insight queries / list_existing_nodes) filters by the resolved scope. Incomplete scope ⇒ zero results, never "see-all" (FR-5).
  • Write-identity guard. EngramaEngine.merge_node and EngramaEngine.merge_relation raise ScopeIncomplete when the effective scope is missing or partial; the MCP boundary translates the request-time ScopeUnresolved into an explicit status: "error" for every write tool (engrama_remember, engrama_relate, engrama_ingest, engrama_sync_note, engrama_sync_vault, engrama_reflect, engrama_approve_insight, engrama_write_insight_to_vault, engrama_reindex). Identity-less writes no longer touch the graph or the vault.
  • Per-request scope resolver. resolve_scope(ctx) reads X-Engrama-Org-Id / X-Engrama-User-Id request headers; both headers present resolves to that scope, both absent falls back to a stable per-install sub_local identity (zero-config standalone, FR-7), exactly one present raises ScopeUnresolved (no silent broadening, FR-3).
  • Relations carry identity. Every edge persists (org_id, user_id) at write time, so a future relation-scoped read can filter without re-walking endpoints (FR-1). SQLite gains nullable edges.org_id / edges.user_id columns plus idx_edges_scope — an idempotent ALTER TABLE runs on connect for pre-Spec-001 databases.
  • SDK scope completion. Engrama(...) zero-config resolves to (sub_local, sub_local). A lone user_id (or lone org_id) is mirrored into the other dimension. A provenance-only override (session_id="…" / agent_id="…" without identity) still falls back to sub_local for (org_id, user_id) while keeping the provenance through.
  • Composite Neo4j indexes. A per-label range index on (n.org_id, n.user_id) for every queried label, in both engrama/backends/neo4j/schema.cypher and scripts/init-schema.cypher (R-6 / NFR-3).
  • Scoped logging context. A contextvars.ContextVar plus logging.Filter injects hashed (sha256[:8]) scope_org / scope_user fields onto every log record emitted inside a request, so an aggregator can group lines by tenant without leaking raw IDs (T009a / NFR-2).
  • CI scope guard. scripts/check_scoped_queries.py walks the backend modules and flags any MATCH ( / FROM nodes / FROM edges literal whose enclosing function neither routes through the scope helper, sits in an auto-exempt write/admin family, nor carries an inline # scope-exempt: <reason> comment. Wired into CI as a warn-only scope-guard job — flipped to blocking once Phases 5–7 land.
  • Migration CLI. engrama migrate tenancy --owner-sub <sub> [--dry-run|--apply] backfills (owner_sub, owner_sub) onto every identity-less node and relation and purges true orphans. Mandatory dry-run first; --report path.json writes an auditable record. Required on any installation that pre-dates this release.

How to upgrade

  1. Stop the MCP server.
  2. Optional: back up the SQLite DB (~/.engrama/engrama.db) or Neo4j volume.
  3. Pick the identity to stamp pre-existing data with — usually the value of ENGRAMA_LOCAL_SUB (or the contents of ~/.engrama/local_sub).
  4. engrama migrate tenancy --owner-sub <sub> --dry-run — review the counts. Re-run with --apply once they look right.
  5. Restart. Reads return data again.

Added

  • Streamable HTTP transport for the MCP server. Switchable via ENGRAMA_TRANSPORT=http (default stays stdio, so existing Claude Desktop setups are untouched). Binds loopback 127.0.0.1:8000 with the MCP endpoint at /mcp, runs stateful so conversational clients keep an Mcp-Session-Id, and validates Origin/Host (DNS-rebinding guard, configurable via ENGRAMA_ALLOWED_ORIGINS). Adds a /health probe and a /.well-known/oauth-protected-resource (RFC 9728) stub wired for the upcoming OAuth phase via ENGRAMA_AUTH_ISSUER. No authentication yet — intended for local use only. See the Streamable HTTP guide.
  • engrama_reindex tool — repairs nodes that are missing their vector embedding (e.g. written while the embedder was unreachable, or predating embeddings). Three phases run as separate calls: detect (scan, read-only), classify (which candidates have embeddable text), and apply (dry_run defaults to true). Heals legacy/corrupted nodes so they become semantically searchable again.

Fixed

  • Silent embedding corruption on write. When an embedder was configured but unreachable at write time (Ollama / OpenAI-compatible cold-start, restart, or network blip), engrama_remember persisted the node without a vector yet still returned status: "ok" — leaving it permanently invisible to semantic search with no signal to anyone. The write path is now honest: the response reports embedded: true|false (with an embedding_note when a vector was deferred), failures log a WARNING that includes the engrama_id, and a vector-less node is healed automatically by an opportunistic sweep on the next successful write — or on demand via engrama_reindex. Proactive writes are still never lost: the node always persists.
  • Inline relations report rejected relation types. engrama_remember's inline relations={...} used to skip a relation type that isn't in the schema with only a server-side log, returning status: "ok" / relations_created: 0 — so the caller couldn't tell a relation had been dropped (the explicit engrama_relate already errors loudly on the same input). The response now includes relations_rejected (and a note) listing the skipped types, while still persisting the node and any valid relations.

[0.12.0] — 2026-05-22

Two latency fixes on the MCP server path plus the documentation site overhaul (bilingual docs, MkDocs landing page, modern GitHub Pages deployment). No API or schema-breaking changes.

Fixed

  • MCP vault I/O no longer blocks the asyncio event loop (#66). The MCP server runs on a single-threaded event loop, but ~15 synchronous ObsidianAdapter calls (list_notes, read_note, add_relation) and direct Path.write_text writes were made straight from inside async tool handlers. While any one of those waited on a slow vault write — common on cloud-sync drives like Proton Drive or OneDrive, and more frequent since 0.11.0 added automatic note creation in engrama_relate — every other tool call queued behind it and hit the client timeout. The server looked half-alive (engrama_status still replied instantly) while engrama_relate / engrama_surface_insights timed out. Every sync vault call inside a tool handler now runs through asyncio.to_thread, freeing the loop to service other tools concurrently.
  • Insight.status range index added to init-schema.cypher (#67). Insight is the highest-cardinality status-bearing label — every engrama_reflect run MERGEs new pending Insights — but it was the one status-indexed label missing from the schema's RANGE INDEXES block. engrama_surface_insights (MATCH (i:Insight {status: $status})) therefore degraded to a label scan once the pending queue grew to a few hundred rows. The index applies hot to existing deployments via CREATE INDEX … IF NOT EXISTS.

Changed

  • Documentation site overhaul. Bilingual docs (English / Castilian Spanish) served from a dedicated MkDocs landing page (index.md) in place of the README fallback, GitHub Pages now deployed via the modern Actions artifact flow rather than the legacy branch push, and the commercial contact points at the website. (#69, #70, #71, #72, #73, #74)

Dependencies

  • Bumped idna (#68) and the GitHub Actions toolchain — actions/cache (#64), actions/upload-artifact (#63), actions/attest-build-provenance (#62).

[0.11.0] — 2026-05-16

Dual-vault contract hardening (#52 Phases A–D), engine + MCP canonicalisation of the merge key against TITLE_KEYED_LABELS (#51,

53, #59), and the one-shot engrama migrate keys command (#54) for

healing rows that pre-date the canonicalisation fix.

Fixed

  • MCP engrama_remember now canonicalises the merge key the same way the engine does (#51 / #53). The handler writes to the async store directly and was carrying its own pre-#51 if "name" in props else "title" selection, so an agent calling engrama_remember(label="Experiment", properties={"name": "X"}) via MCP still landed on a name-keyed row even after the engine was fixed. Now the handler runs the same TITLE_KEYED_LABELS canonicalisation as EngramaEngine.merge_node; the non-canonical alias is silently dropped (matching the sanitiser pattern). New tests/test_mcp_remember_merge_key.py pins the MCP-boundary contract that #51's engine-level tests didn't cover.
  • engrama_sync_vault iteration shape. ObsidianAdapter.list_notes() returns [{"path": ..., "name": ...}, ...] but the MCP handler was iterating those entries as if they were strings, so every note path reaching obsidian.read_note() was a dict — the resulting WindowsPath / dict TypeError was logged at WARNING level and the note was bumped into the skipped bucket. Vault sync had been a no-op end-to-end for some time without a regression test. Surfaced while writing the Phase D dry-run tests; iteration now unpacks the dict at the top of the loop.

Added

  • engrama migrate keys CLI command — one-shot migration that heals rows whose key_field doesn't match TITLE_KEYED_LABELS, the residue from pre-#53 / pre-#59 writes that picked the wrong merge key from the caller's property bag. Default is a dry-run that prints the plan and exits; --apply rewrites the rows. On SQLite the migration is a rename-in-place because UNIQUE(label, key_value) already collapsed the two writes onto a single row at write time. On Neo4j the migrator detects sibling-collision cases (where a canonical-keyed node already carries the same identity) and skips them with an actionable error rather than tripping the uniqueness constraint. Optional --labels Foo,Bar scopes the run; --report PATH writes a full JSON audit trail. Closes #54.
  • dry_run parameter on engrama_sync_vault and engrama_sync_note. When true, neither tool writes to the graph or injects engrama_id into note frontmatter. They return the same JSON envelope as a real run, with created/updated counts replaced by would_create / would_update. The vault response also includes files_would_receive_engrama_id — the explicit list of notes that would be modified. Gives operators a cheap way to confirm a sync targets the intended vault before it commits any writes, and is the natural pair to engrama_status (Phase C) when reasoning about a multi-MCP setup. Phase D of #52.
  • engrama_status MCP tool — read-only introspection that returns the running server's vault path, backend, embedder, search mode and engrama version. Designed to be called at session start when Engrama coexists with other Obsidian-capable MCPs (e.g. obsidian-mcp) so the agent can disambiguate which server "the vault" refers to before any sync or ingest call — a server-side signal that doesn't rely on docstrings being read or on engrama-system-prompt-v0.5.md being installed. JSON contract documented in architecture.md. Phase C of #52.

Changed

  • MCP tool descriptions for sync/ingest declare their vault scope. engrama_sync_vault, engrama_sync_note and engrama_ingest now state that they operate against Engrama's internal vault (VAULT_PATH), distinct from any user-managed Obsidian vault exposed by a separate obsidian-mcp server. Pure docstring change — no behavioural difference, but gives the agent a server-side signal to disambiguate when multiple Obsidian-capable MCPs are connected. First step of the multi-phase contract hardening tracked in #52.

Fixed

  • Engine merge_node honours TITLE_KEYED_LABELS regardless of the caller's property bag. Previously the engine picked the merge key from whichever of name or title was present in properties, so a caller that put name in the bag for a title-keyed label (notably the MCP engrama_remember tool, which forwards params.properties verbatim) bypassed the schema convention. The result was duplicate rows on Neo4j and silent property-key divergence on SQLite between SDK and MCP writes of the same logical node. The engine now canonicalises the merge key after sanitisation; the non-canonical alias is silently dropped (matching the sanitiser's behaviour with reserved keys). Existing duplicate rows from earlier writes are not healed — a one-shot engrama migrate keys command is tracked as a follow-up. (#51)
  • Insight added to TITLE_KEYED_LABELS. The auto-injected Insight dataclass uses title as its merge key (Neo4j constraint enforces n.title IS UNIQUE, every reflect/proactive query filters by title), but the codegen only added user-defined nodes to TITLE_KEYED_LABELS, so Insight was missing. The engine fix above exposed the mismatch by canonicalising title → name for any label not in the set, breaking every Cypher MATCH (i:Insight {title: ...}). Patched both engrama/core/schema.py and the generator scripts so future regenerations include Insight.

[0.10.0] — 2026-05-14

CI maturity + supply-chain hardening for the first public PyPI publish. The library APIs are unchanged from 0.9.0; this release is about making engrama installable, auditable, and migratable for users outside the dev environment.

Added

  • engrama export / engrama import — backend-agnostic NDJSON dump and restore for the graph + vectors, enabling cross-backend migrations (SQLite ↔ Neo4j) as a first-class CLI path. Format is one JSON object per line: envelope, then node / relation / vector records. Vectors only restore when source and target embedding dimensions match; mismatches are reported and engrama reindex rebuilds them under the active embedder. (#30)
  • Release pipeline (.github/workflows/release.yml) — six-stage guardian → build → sbom → attest → publish → release-notes triggered by v* tags. PyPI trusted publishing (OIDC, no API key in secrets), CycloneDX + SPDX SBOMs attached to the GitHub Release, SLSA build-provenance attestations on the wheel and sdist, and a workflow_dispatch dry-run path. Guardian fails fast on version drift between pyproject.toml, engrama/__init__.py and the topmost CHANGELOG entry. (#27)
  • PR-level vulnerability gateaudit-deps job runs pip-audit on PRs that touch pyproject.toml or uv.lock (always on push to main). Blocks on CVSS ≥ 7.0 (looked up from OSV) or any advisory with an upstream fix; warns on LOW/MEDIUM with no fix. (#27)
  • CI matrix across Python 3.11 / 3.12 / 3.13 for import-smoke and test-sqlite. New test-neo4j integration job uses a neo4j:5.26.4-community service container with the committed scripts/init-schema.cypher applied via the Python driver. (#26)
  • Phase-1 CI baselinelint (ruff format + check), test-sqlite (SQLite-only suite, no Docker), import-smoke (DDR-004 promise gate). Dependabot weekly for pip and github-actions, monthly for docker. (#7–#12)
  • Phase-4 repo hygiene.github/CODEOWNERS, pull_request_template.md, structured issue templates, security.md (disclosure via GitHub private advisories), lychee-action weekly + per-PR link checker, DDR-template.md. (#22)

Changed

  • README embedder section — expanded to a six-provider matrix (Ollama, OpenAI, LM Studio, vLLM, llama.cpp, Jina) with copy-pasteable .env blocks, recommended models + dimensions, start commands where relevant, and provider-specific gotchas. Mirrored in README_ES.md. (#29)
  • Misconfig surfacing in the CLI/MCPGRAPH_BACKEND=neo4j without the [neo4j] extra (#23) and engrama-mcp without the [mcp] extra (#28) now both emit a single-line install hint to stderr and exit 1, replacing the prior raw Python tracebacks.
  • Documentation — install instructions aligned with source-only reality, Codex + ChatGPT Desktop MCP setup snippets added in both READMEs. (#15, #23, #25)
  • License metadatapyproject.toml corrected to Apache-2.0.

Fixed

  • Base install no longer eagerly imports neo4jengrama/core/client.py defers the import to EngramaClient.__init__, so import engrama works on a pip install engrama base install with no extras (DDR-004 promise gate). (#11)
  • FTS5 MATCH sanitization on SQLite — hyphenated queries like engrama-mcp-server are wrapped as phrases instead of being treated as FTS5 grammar, restoring the fulltext path. 14 tests added. (#16)
  • Hybrid search degraded-mode signal — when the embeddings provider is unreachable, the search engine exposes last_mode = {mode, degraded, reason} and the MCP engrama_search response carries a search_mode field, so callers can distinguish a fulltext-only fallback from a healthy hybrid result. (#20)
  • Degenerate embeddings caught at write timeengrama/embeddings/health.is_degenerate_vector flags needs_reindex=true on the node and skips vector storage; list_nodes_for_embedding(force=False) now pulls those nodes back so engrama reindex heals them. (#21)
  • under_connected reflect pattern excludes stub neighbours — both backends now filter out status='stub' nodes when counting connections. (#19)
  • Ruff import grouping repo-wide cleanup. (#24)

[0.9.0] — 2026-05-10

Portable storage — SQLite + sqlite-vec as the default backend, Neo4j moved to an opt-in extra, single OpenAI-compatible embedder, full async contract parity (DDR-004, PR #5).

Added

  • SQLite backend (engrama/backends/sqlite/) — full implementation of the GraphStore and VectorStore protocols on top of sqlite3 and the sqlite-vec extension. Modules: store.py (sync graph store, 36+ methods, FTS5 fulltext), async_store.py (async wrapper that mirrors Neo4jAsyncStore's rich return shapes), vector.py (SqliteVecStore using the vec0 virtual table), schema.sql (auto-applied on first connect). Default DB path: ~/.engrama/engrama.db (override via ENGRAMA_DB_PATH).
  • OpenAI-compatible embedding provider (engrama/embeddings/openai_compat.py) — single client speaking the OpenAI /v1/embeddings shape, drives Ollama, OpenAI proper, LM Studio, vLLM, llama.cpp, Jina, and any future compatible service. Sync (embed, embed_batch) and async (aembed, aembed_batch) methods.
  • Backend factory (engrama/backends/__init__.py) — create_stores() and create_async_stores() dispatch the engine, CLI, SDK, and MCP server through a single entry point. GRAPH_BACKEND env var (or explicit config dict) selects the implementation. Skills and tools no longer hardcode any backend.
  • engrama-mcp CLI flags--backend {sqlite,neo4j} (default sqlite), --db-path, --neo4j-uri, --neo4j-password, --neo4j-database, --vault-path. Defaults read from environment.
  • Async GraphStore contract suite (tests/contracts/test_async_graphstore_contract.py) — parameterised over sqlite-async and neo4j-async. Pins the rich response shapes the MCP server depends on (merge_node returns {"node": ..., "created": ...}, neighbours come back as {label, name, via, properties}, etc.). 12 tests × 2 backends.
  • get_approved_titles on every store layer (Neo4j async, SQLite async, SQLite sync) — used by reflect to skip patterns the user has already approved (see Fixed below).
  • 76 SQLite-only tests (tests/backends/test_sqlite*.py, tests/contracts/test_graphstore_contract.py SQLite branch) — pass on a fresh checkout with no .env and no Docker. CI runs them by default.
  • backends.md — newcomer-facing decision guide: when to pick SQLite, when to pick Neo4j, how to switch, FAQ. Linked from README and ARCHITECTURE.
  • DDR-004 — formal record of the portable storage decision, including the three regressions found and fixed during e2e testing.

Changed

  • Default GRAPH_BACKEND is now sqlite (was neo4j). Existing installs that rely on Neo4j must set GRAPH_BACKEND=neo4j explicitly.
  • neo4j driver moves to an opt-in extra (uv sync --extra neo4j, or the engrama[neo4j] extra once Engrama ships on PyPI). Base install ships with sqlite-vec, httpx, pydantic, python-dotenv, pyyaml only.
  • Neo4jGraphStore returns plain dicts at the boundary (Phase 1 of the spec). EngramaEngine, recall.py and other internal callers consume Python dict rather than driver-specific types.
  • HybridSearchEngine copies enrichment fields (summary, tags, confidence, updated_at) onto SearchResult for vector-only hits. Both backends' search_similar now project these fields.
  • engrama init is a no-op on SQLite for Cypher schema statements (the schema is in backends/sqlite/schema.sql, applied automatically) but still seeds domain nodes from the active profile.
  • Documentation overhaul — README, README_ES, ARCHITECTURE, ROADMAP, VISION, GRAPH-SCHEMA, CONTRIBUTING and GLOSARIO_ES all updated to reflect the dual-backend reality. .env.example now defaults to the zero-dep SQLite path with Neo4j commented as opt-in.

Fixed

  • Async store contract drift on SQLite. SqliteAsyncStore was forwarding sync calls via __getattr__, leaking the legacy [{"n": ...}] shape to the MCP server which expected the rich Neo4jAsyncStore-style {"node": ..., "created": ...}. Crashed any engrama_remember call against the SQLite backend. Replaced with explicit method-by-method delegation that translates shapes; locked in by the new async contract suite.
  • Reflect overwriting approved Insights. Each _build_* helper in engrama_reflect called merge_node with status="pending", silently undoing user approvals on re-runs. Reflect now filters candidates against dismissed | approved. The output payload also exposes approved_count next to dismissed_count.
  • Search dropping enrichment on pure-semantic hits. The hybrid scorer only copied summary/tags from fulltext results, so nodes ranked solely by vector similarity surfaced with empty fields. search_similar now projects the enrichment fields and the scorer copies them on the vector path.

Removed

  • Implicit assumption that Neo4j is required to use Engrama. Neo4j is still fully supported via the neo4j extra; nothing about its feature surface has changed.

[0.8.0] — 2026-04-14

Temporal reasoning — confidence decay, fact supersession, and time-travel queries (DDR-003 Phase D).

Added

  • Confidence decay: decay_confidence() in Neo4jAsyncStore applies exponential decay (confidence × exp(-rate × days_old)) to stale nodes. Supports dry-run mode, label filtering, and auto-archival of nodes below a confidence threshold. Sync equivalent was already in Neo4jGraphStore.decay_scores().
  • valid_to support: merge_node() now accepts valid_to to mark facts as superseded. Setting valid_to auto-halves confidence. Updating a superseded node clears valid_to (revival) and returns a conflict warning.
  • Temporal queries: query_at_date(date, label?) in both async and sync stores — returns nodes valid at a specific date (valid_from <= date AND (valid_to IS NULL OR valid_to >= date)).
  • Enhanced CLI engrama decay: --dry-run now shows a sample table of nodes that would be affected with current vs projected confidence values.
  • Enhanced stale knowledge detection: reflect stale_knowledge pattern now also considers nodes with confidence < 0.3 as stale regardless of age. Insight body includes confidence value for severity assessment.
  • Async store tests: TestAsyncDecayConfidence (5 tests), TestAsyncValidTo (3 tests), TestAsyncQueryAtDate (3 tests) added to test_temporal.py.

Changed

  • architecture.md: rewritten 5-layer diagram (Adapters → Skills → Engine → Protocols → Backends), added temporal reasoning section, configuration reference table, updated directory structure to match reality (15 test files).
  • graph-schema.md: added temporal fields (valid_to, decayed_at, embedding) to all-nodes section.
  • temporal.py: days_since() now handles Neo4j DateTime objects via .to_native().
  • search.py: min-max normalization returns 1.0 for single-result sets (was 0.0).

Fixed

  • Search normalization bug: a single search result normalized to score 0.0 instead of 1.0.
  • Neo4j DateTime incompatibility in days_since(): TypeError when subtracting neo4j.time.DateTime from datetime.datetime.
  • valid_to and caller-supplied valid_from now stored as Neo4j datetime() instead of raw strings, fixing query_at_date comparisons.

[0.7.0] — 2026-04-13

Async embedding providers and hybrid search (DDR-003 Phase B + C).

Added

  • Async embedding methods: OllamaProvider now has aembed(), aembed_batch(), ahealth_check(), aclose() using httpx.AsyncClient. Sync methods (urllib) remain for CLI/SDK backward compatibility. NullProvider also has async counterparts.
  • Async hybrid search: HybridSearchEngine.asearch() — async counterpart of search(). Uses aembed() and async store methods. Deployed in MCP server for non-blocking search.
  • Embed-on-write: engrama_remember and engrama_sync_note MCP tools now embed nodes automatically when EMBEDDING_PROVIDER is configured. Uses async aembed() to avoid blocking the event loop.
  • core/text.py — re-export of embeddings/text.py for import convenience.
  • httpx>=0.27 added as optional dependency (embeddings and mcp extras).
  • Test suite: test_hybrid_search.py (unit tests with mock stores for sync+async search, scoring formula, graceful degradation, integration tests with real Neo4j+Ollama). Async embedding tests added to test_embeddings.py (NullProvider + OllamaProvider async methods).

Changed

  • MCP server lifespan: simplified — no longer creates redundant sync stores for hybrid search. The async store serves as both GraphStore and VectorStore for HybridSearchEngine.asearch().
  • MCP engrama_search: uses HybridSearchEngine.asearch() with the async store directly, eliminating the sync→async impedance mismatch.
  • MCP embed-on-write: uses await embedder.aembed() instead of sync embedder.embed(), preventing event loop blocking.
  • architecture.md: added "Embedding and hybrid search" section documenting dual-mode providers, embed-on-write, vector index strategy, and scoring formula.

Fixed

  • Event loop blocking in MCP server: embedding and hybrid search previously called sync methods from async context, blocking the event loop. Now fully async.

[0.6.0] — 2026-04-13

Protocol extraction and bug fixes (DDR-003 Phase A). Zero Cypher in server.py.

Added

  • DDR-003 Phase A — Protocol layer: abstract GraphStore, VectorStore, EmbeddingProvider protocols in core/protocols.py. All storage operations route through backend implementations — no adapter, skill, or tool writes Cypher directly.
  • Neo4jAsyncStore (backends/neo4j/async_store.py) — async backend for the MCP server. Contains all Cypher that was previously inline in server.py. Methods: merge_node, get_node, delete_node, merge_relation, get_neighbours, get_node_with_neighbours, fulltext_search, count_labels, run_pattern, lookup_node_label, plus vector ops (store_embedding, search_similar, delete_embedding, count_embeddings) and Insight ops (get_dismissed_titles, get_pending_insights, get_insight_by_title, update_insight_status, mark_insight_synced, find_insight_by_source_query, list_existing_nodes).
  • create_async_store() factory in backends/__init__.py — reads EMBEDDING_DIMENSIONS from config/env, returns configured Neo4jAsyncStore.
  • count_labels() and close() methods on sync Neo4jGraphStore.
  • NullGraphStore and NullVectorStore — no-op implementations for testing and dry-run mode.
  • Test suites: test_protocols.py (protocol conformance for all stores, NullGraphStore/NullVectorStore behaviour, async store method inventory) and test_neo4j_store.py (integration tests against real Neo4j: merge, dedup, update, relations, fulltext COALESCE, neighbours, count_labels, run_cypher, get/delete node, health_check).
  • .env.example: HYBRID_ALPHA (fulltext vs vector weight) and HYBRID_GRAPH_BETA (graph topology boost).

Fixed

  • BUG-006: engrama_search returned null names for title-keyed nodes (Decision, Problem, Vulnerability, etc.). Fulltext search and neighbour queries now use COALESCE(node.name, node.title).
  • BUG-007: engrama_reflect generated duplicate under-connected Insights on repeated runs. _detect_under_connected now checks for existing pending/approved Insight by source_query before creating; updates in place if found; respects previously dismissed Insights.
  • BUG-008: engrama_context showed duplicate relation entries in the via array. Deduplicated in get_node_with_neighbours.

Changed

  • server.py: rewired from inline Cypher to Neo4jAsyncStore method calls. Contains zero Cypher strings (was ~2053 lines, now ~1753). MCP tools handle orchestration, validation, vault I/O, and response formatting only.
  • server.py lifespan: creates Neo4jAsyncStore via create_async_store(driver, database, config) and stores it in context alongside the raw driver.
  • architecture.md: updated stack table, directory structure, and added "Protocol layer (DDR-003 Phase A)" section documenting sync/async stores, null implementations, and factory pattern.

[0.5.0] — 2026-04-12

Three core features that make Engrama valuable beyond a raw Neo4j wrapper. System prompt v0.5.

Added

  • Phase 1 — Ingestion: engrama_ingest MCP tool. Reads a vault note, raw text, or conversation transcript and returns content with entity extraction guidance. The agent extracts entities and calls engrama_remember for each one. Includes graph deduplication hints (existing nodes listed in response).
  • Phase 2 — Adaptive Reflect: engrama_reflect now inspects the graph before querying. Selects only applicable patterns based on what labels have data. Four new detection patterns: technique transfer (cross-domain technique applicability), concept clustering (3+ entities sharing a Concept), stale knowledge (90+ day old nodes linked to active Projects), under-connected nodes (<2 relationships). Previously dismissed Insights are never re-surfaced. Confidence scoring based on connection strength and entity count.
  • Phase 3 — Proactivity: Session state tracks engrama_remember calls. After 10+ entities stored since last reflect, engrama_remember returns a proactive_hint suggesting the agent run reflect. engrama_search checks for pending Insights related to the search query and surfaces them inline. engrama_reflect resets the counter.
  • Reference docs: Extracted v0.4 detailed content into docs/reference/ (faceted-classification, query-patterns, node-schema, sync-contract). System prompt v0.5 is lean; reference docs are the "workshop manual".
  • DDR-001: Design decision record for the faceted classification system.

Fixed

  • Phase 3 proactivity counter not firing: _proactive_state moved from FastMCP lifespan context (not reliably mutable across tool calls) to a module-level dict. Counter now persists correctly across engrama_remember invocations.
  • training_opportunity never activating: query only matched Problem {status: "open"} but real graphs have Vulnerability nodes (status: "demonstrated"). Broadened WHERE clause: (issue:Vulnerability) OR (issue:Problem AND issue.status = $open_status).
  • shared_technology skipped in most graphs: required both Project AND Technology labels, but many graphs have Courses or Decisions sharing technologies. Broadened: matches any entity via USES/TEACHES/COMPOSED_OF, activation requires only Technology label.
  • stale_knowledge skipped when only Courses exist: activation required Project but the query also checks Course connections. Broadened: activates when either Project OR Course exists.
  • _run_pattern too rigid for OR-logic: added any_labels parameter — each entry is an OR-group where at least one label must have data. Used by training_opportunity (Problem OR Vulnerability + Course) and stale_knowledge (Project OR Course).

Changed

  • System prompt v0.5: shorter, token-efficient. Adds dual-vault routing (obsidian-mcp vs engrama). References docs/reference/ for details.
  • engrama_search response now wraps results in {"results": [...]} object (was bare array) to accommodate optional pending_insights and proactive_hint fields.
  • Reflect skill confidence scores adjusted: cross-project 0.85, shared-tech 0.7, training 0.65, technique-transfer 0.5–0.9 (scaled by related entities), concept-clustering 0.5–0.9 (scaled by count), stale 0.5, under-connected 0.4.

[0.4.0] — 2026-04-12

Bug-fix sprint + schema expansion. System prompt v0.4.

Fixed

  • BUG-001: CLI init dropped fulltext index — comment lines inside ;-split chunks caused the entire CREATE FULLTEXT INDEX statement to be silently discarded.
  • BUG-002: engrama_remember never created vault notes — nodes had no obsidian_path, breaking the DDR-002 contract. Now creates full YAML frontmatter with engrama_id, type, properties, and empty relations block.
  • BUG-003: engrama_relate failed to write to vault because obsidian_path was always null (cascading from BUG-002). Added fallback: if source node has no vault note, create one on-the-fly.
  • BUG-005: engrama_remember crashed when relations dict was passed inside properties — Neo4j rejects Map values as node properties. Now extracts relations before MERGE and merges both input paths (top-level field and nested in properties) into a single processing loop.

Added

  • BUG-004: Domain seed data for all modules — engrama init now seeds domain nodes and key concepts for hacking, teaching, photography, and AI modules.
  • BUG-005: Inline relations in engrama_remember — pass relations: {TEACHES: [Python]} and targets are found/created (with stub creation) + relationships merged + vault frontmatter written, all in one call.
  • FIX-008: Material node type for teaching artifacts (cheatsheets, slides, exercises, reference cards). Properties: name, type, format, status, notes. New relation: HAS_MATERIAL (Course → Material).
  • DDR-001: Design decision record for the faceted classification system (was referenced but missing).

Changed

  • FIX-006: System prompt section 4 — relaxed "immediately call relate" to reflect that remember now supports inline relations.
  • FIX-007: System prompt section 3 — INSTANCE_OF is now mandatory only for Problem, Decision, Vulnerability. Recommended for all other types when it adds discovery value.
  • System prompt version bumped to 0.4.0. File renamed from v0.3 to v0.4.

Removed

  • _obsidian_mcp_ref/ — development reference folder, not part of the codebase.
  • .claude/ — session working directory artifact.

[0.3.0] — 2026-04-12

Bidirectional sync and vault portability (DDR-002).

Added

  • DDR-002: Bidirectional sync — all graph relations are serialized into each note's YAML frontmatter relations map. Vault and graph are co-equal sources of truth.
  • ObsidianAdapter.add_relation(), remove_relation(), set_relations() — idempotent frontmatter relation management.
  • NoteParser extracts relations from frontmatter, normalises scalars to lists, uppercases relation types.
  • ObsidianSync.full_scan() three-pass strategy: nodes → wiki-links → frontmatter relations.
  • _infer_stub_label() — maps relation types to likely target labels for stub node creation.
  • AssociateSkill writes relations to vault frontmatter (dual-write contract).
  • DDR-002 test suite: TestParserRelations (3 tests), TestAdapterRelations (9 tests).

[0.2.0] — 2026-04-11

Faceted classification system (DDR-001).

Added

  • DDR-001: Six-facet classification adapted from Ranganathan's PMEST + BFO. Facets: identity, composition, action, purpose, context, domain.
  • Composable profiles: base.yaml + domain modules (hacking, teaching, photography, ai).
  • generate_from_profile.py — merges profiles, deduplicates relations, generates schema.py and init-schema.cypher.
  • CLI: engrama init --profile base --modules hacking teaching photography ai.
  • System prompt v0.2 with full faceted classification documentation.

[0.1.0] — 2026-04-10

Initial release. Phases 0–7: core engine, MCP adapter, Obsidian sync, skills, reflect, proactive insights, SDK, CLI.

Added

  • Neo4j 5.26 LTS with Docker Compose setup.
  • Core engine: MERGE semantics, timestamps, fulltext search.
  • FastMCP server with 10 tools: search, remember, relate, context, sync_note, sync_vault, reflect, surface_insights, approve_insight, write_insight_to_vault.
  • Obsidian adapter: vault ↔ graph sync via engrama_id.
  • Four skill classes: RememberSkill, RecallSkill, AssociateSkill, ForgetSkill.
  • ReflectSkill: cross-project solution transfer, shared technology detection, training opportunity discovery.
  • ProactiveSkill: surface/approve/dismiss/write Insights to vault.
  • Python SDK: Engrama class wrapping all skills.
  • CLI: engrama init, engrama verify, engrama reflect, engrama search.
  • 100 integration tests across 9 test files.