Changelog¶
All notable changes to Engrama will be documented here.
Format: Keep a Changelog Versioning: Semantic Versioning
[Unreleased]¶
Added¶
mcp.jsonserver manifest for marketplace / public-build publication. It declares the server metadata, install commands, a Claude Desktop drop-inmcpServersblock, the configurable environment variables, and the full catalog of all 14 MCP tools with their JSON-SchemainputSchemaand tool annotations. The version is kept in lock-step withpyproject.toml.examples/fleshed out — a runnable Python SDK quickstart (examples/python_sdk/quickstart.py), a LangChain tool-wrapping example, anexamples/README.mdindex, and aclaude_desktop/config.dev.jsonvariant 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_insightsacceptstitle/queryfilters.titlereturns one pending Insight by exact title;queryreturns 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'sengrama_id.
Changed¶
- Archived nodes are excluded from search.
fulltext_search(SQLite + Neo4j) now filters outstatus='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_hintfor pending Insights now matches the real queue. It was computed from a separate fulltext search filtered totype=='Insight'— with nostatusfilter — so it fired on approved, dismissed or synced Insights too, sending the agent toengrama_surface_insightsfor a queue that came back empty (false positive). It also projectedtitle/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) asengrama_surface_insightsand 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 matchedengrama_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)(andengrama_status'sstartup_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_errorhelper 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_forgettool — 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 injectsX-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 + γ × temporaltorrf + β × 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_boostis replaced by a graph-distance signal (result-set cohesion + optional query-anchor proximity), computed only over the fused candidate window. - The
engrama_searchpayload now exposes the per-resultrrf_scoreandgraph_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 viaENGRAMA_RANKING_LEGACY=1. - Inline relations resolve by confidence, not silent stubbing (#93). When
engrama_remembergets 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_ambiguouswith adid_you_meanlist 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_contextno 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"); aNonescope keeps the admin/debug fetch-by-key. Adds thenode_visiblescope 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-storingnodes_ftsFTS5 table, so the erased subject's indexed text (name/title/description/notes/…) survived on disk after a "permanent" delete. The erasure now removes the matchingnodes_ftsrows too, matching every other hard-delete path. Neo4j was unaffected (DETACH DELETEis atomic). - Caller-supplied property keys are backtick-quoted before reaching Cypher.
A node's property key names (free-form via the
engrama_rememberpropertiesbag) were interpolated raw intoSET n.<key> = $p0in 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 withescape_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/--labelsare validated against the schema whitelist.engrama decayandengrama migrate keyspassed 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 keysnow reject any label that is not a knownNodeTypebefore it can reach a query (EngramaEngine.decay_scoresvalidates too, covering SDK callers). merge_relationnow scope-filters both endpoints (#93). Endpoint matching was unscoped whilelookup_node_labelwas tenant-filtered. That asymmetry let a relation write reach a node owned by another tenant — forming a cross-tenant edge, and lettingengrama_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=Nonestill 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_reindexscan is now tenant-scoped.list_unembedded_nodestakes an optional scope; the reindex tool passes the request's resolved scope, sodetect/classifyonly sample the caller's own vector-less nodes andapplyonly 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 viascope=None.
Fixed¶
engrama_searchno 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 raiseNeo.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 reachingdb.index.fulltext.queryNodesin both the sync and async Neo4j stores, so every term is matched literally (CI/CDstill tokenises intoci+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)waylookup_node_labelresolves them, instead of a single statically re-derived key. Lookup and merge can no longer disagree: a node keyed bytitlewhileTITLE_KEYED_LABELSpredictsname(or differing only in case) now connects instead of silently failing. Therelations_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: 0with only a server-log warning —relations_failedlists targets where no edge was created, with an explanatory_notemirroringrelations_rejected. engrama_relatepinpoints the failing endpoint (#93). A failed relate now returns a structuredstatus: "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, andgraph-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.0was the earlier non-breaking docs/latency release, so the breaking work takes0.13.0.1.0.0is 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 underspecs/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_nodeandEngramaEngine.merge_relationraiseScopeIncompletewhen the effective scope is missing or partial; the MCP boundary translates the request-timeScopeUnresolvedinto an explicitstatus: "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)readsX-Engrama-Org-Id/X-Engrama-User-Idrequest headers; both headers present resolves to that scope, both absent falls back to a stable per-installsub_localidentity (zero-config standalone, FR-7), exactly one present raisesScopeUnresolved(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 nullableedges.org_id/edges.user_idcolumns plusidx_edges_scope— an idempotentALTER TABLEruns on connect for pre-Spec-001 databases. - SDK scope completion.
Engrama(...)zero-config resolves to(sub_local, sub_local). A loneuser_id(or loneorg_id) is mirrored into the other dimension. A provenance-only override (session_id="…"/agent_id="…"without identity) still falls back tosub_localfor(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 bothengrama/backends/neo4j/schema.cypherandscripts/init-schema.cypher(R-6 / NFR-3). - Scoped logging context. A
contextvars.ContextVarpluslogging.Filterinjects hashed (sha256[:8])scope_org/scope_userfields 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.pywalks the backend modules and flags anyMATCH (/FROM nodes/FROM edgesliteral 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-onlyscope-guardjob — 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.jsonwrites an auditable record. Required on any installation that pre-dates this release.
How to upgrade¶
- Stop the MCP server.
- Optional: back up the SQLite DB (
~/.engrama/engrama.db) or Neo4j volume. - Pick the identity to stamp pre-existing data with — usually the value of
ENGRAMA_LOCAL_SUB(or the contents of~/.engrama/local_sub). engrama migrate tenancy --owner-sub <sub> --dry-run— review the counts. Re-run with--applyonce they look right.- Restart. Reads return data again.
Added¶
- Streamable HTTP transport for the MCP server. Switchable via
ENGRAMA_TRANSPORT=http(default staysstdio, so existing Claude Desktop setups are untouched). Binds loopback127.0.0.1:8000with the MCP endpoint at/mcp, runs stateful so conversational clients keep anMcp-Session-Id, and validatesOrigin/Host(DNS-rebinding guard, configurable viaENGRAMA_ALLOWED_ORIGINS). Adds a/healthprobe and a/.well-known/oauth-protected-resource(RFC 9728) stub wired for the upcoming OAuth phase viaENGRAMA_AUTH_ISSUER. No authentication yet — intended for local use only. See the Streamable HTTP guide. engrama_reindextool — 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), andapply(dry_rundefaults totrue). 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_rememberpersisted the node without a vector yet still returnedstatus: "ok"— leaving it permanently invisible to semantic search with no signal to anyone. The write path is now honest: the response reportsembedded: true|false(with anembedding_notewhen a vector was deferred), failures log a WARNING that includes theengrama_id, and a vector-less node is healed automatically by an opportunistic sweep on the next successful write — or on demand viaengrama_reindex. Proactive writes are still never lost: the node always persists. - Inline relations report rejected relation types.
engrama_remember's inlinerelations={...}used to skip a relation type that isn't in the schema with only a server-side log, returningstatus: "ok"/relations_created: 0— so the caller couldn't tell a relation had been dropped (the explicitengrama_relatealready errors loudly on the same input). The response now includesrelations_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
ObsidianAdaptercalls (list_notes,read_note,add_relation) and directPath.write_textwrites were made straight from insideasynctool 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 inengrama_relate— every other tool call queued behind it and hit the client timeout. The server looked half-alive (engrama_statusstill replied instantly) whileengrama_relate/engrama_surface_insightstimed out. Every sync vault call inside a tool handler now runs throughasyncio.to_thread, freeing the loop to service other tools concurrently. Insight.statusrange index added toinit-schema.cypher(#67).Insightis the highest-cardinality status-bearing label — everyengrama_reflectrun 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 viaCREATE 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_remembernow 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-#51if "name" in props else "title"selection, so an agent callingengrama_remember(label="Experiment", properties={"name": "X"})via MCP still landed on aname-keyed row even after the engine was fixed. Now the handler runs the sameTITLE_KEYED_LABELScanonicalisation asEngramaEngine.merge_node; the non-canonical alias is silently dropped (matching the sanitiser pattern). Newtests/test_mcp_remember_merge_key.pypins the MCP-boundary contract that #51's engine-level tests didn't cover. engrama_sync_vaultiteration shape.ObsidianAdapter.list_notes()returns[{"path": ..., "name": ...}, ...]but the MCP handler was iterating those entries as if they were strings, so every note path reachingobsidian.read_note()was a dict — the resultingWindowsPath / dictTypeErrorwas logged at WARNING level and the note was bumped into theskippedbucket. 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 keysCLI command — one-shot migration that heals rows whosekey_fielddoesn't matchTITLE_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;--applyrewrites the rows. On SQLite the migration is a rename-in-place becauseUNIQUE(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,Barscopes the run;--report PATHwrites a full JSON audit trail. Closes #54.dry_runparameter onengrama_sync_vaultandengrama_sync_note. Whentrue, neither tool writes to the graph or injectsengrama_idinto note frontmatter. They return the same JSON envelope as a real run, withcreated/updatedcounts replaced bywould_create/would_update. The vault response also includesfiles_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 toengrama_status(Phase C) when reasoning about a multi-MCP setup. Phase D of #52.engrama_statusMCP 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 onengrama-system-prompt-v0.5.mdbeing installed. JSON contract documented inarchitecture.md. Phase C of #52.
Changed¶
- MCP tool descriptions for sync/ingest declare their vault scope.
engrama_sync_vault,engrama_sync_noteandengrama_ingestnow state that they operate against Engrama's internal vault (VAULT_PATH), distinct from any user-managed Obsidian vault exposed by a separateobsidian-mcpserver. 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_nodehonoursTITLE_KEYED_LABELSregardless of the caller's property bag. Previously the engine picked the merge key from whichever ofnameortitlewas present inproperties, so a caller that putnamein the bag for a title-keyed label (notably the MCPengrama_remembertool, which forwardsparams.propertiesverbatim) 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-shotengrama migrate keyscommand is tracked as a follow-up. (#51) Insightadded toTITLE_KEYED_LABELS. The auto-injectedInsightdataclass usestitleas its merge key (Neo4j constraint enforcesn.title IS UNIQUE, everyreflect/proactivequery filters bytitle), but the codegen only added user-defined nodes toTITLE_KEYED_LABELS, soInsightwas missing. The engine fix above exposed the mismatch by canonicalisingtitle → namefor any label not in the set, breaking every CypherMATCH (i:Insight {title: ...}). Patched bothengrama/core/schema.pyand the generator scripts so future regenerations includeInsight.
[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, thennode/relation/vectorrecords. Vectors only restore when source and target embedding dimensions match; mismatches are reported andengrama reindexrebuilds them under the active embedder. (#30)- Release pipeline (
.github/workflows/release.yml) — six-stageguardian → build → sbom → attest → publish → release-notestriggered byv*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 aworkflow_dispatchdry-run path. Guardian fails fast on version drift betweenpyproject.toml,engrama/__init__.pyand the topmost CHANGELOG entry. (#27) - PR-level vulnerability gate —
audit-depsjob runspip-auditon PRs that touchpyproject.tomloruv.lock(always on push tomain). 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-smokeandtest-sqlite. Newtest-neo4jintegration job uses aneo4j:5.26.4-communityservice container with the committedscripts/init-schema.cypherapplied via the Python driver. (#26) - Phase-1 CI baseline —
lint(ruff format + check),test-sqlite(SQLite-only suite, no Docker),import-smoke(DDR-004 promise gate). Dependabot weekly forpipandgithub-actions, monthly fordocker. (#7–#12) - Phase-4 repo hygiene —
.github/CODEOWNERS,pull_request_template.md, structured issue templates,security.md(disclosure via GitHub private advisories),lychee-actionweekly + 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
.envblocks, recommended models + dimensions, start commands where relevant, and provider-specific gotchas. Mirrored inREADME_ES.md. (#29) - Misconfig surfacing in the CLI/MCP —
GRAPH_BACKEND=neo4jwithout the[neo4j]extra (#23) andengrama-mcpwithout 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 metadata —
pyproject.tomlcorrected toApache-2.0.
Fixed¶
- Base install no longer eagerly imports
neo4j—engrama/core/client.pydefers the import toEngramaClient.__init__, soimport engramaworks on apip install engramabase install with no extras (DDR-004 promise gate). (#11) - FTS5 MATCH sanitization on SQLite — hyphenated queries like
engrama-mcp-serverare 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 MCPengrama_searchresponse carries asearch_modefield, so callers can distinguish a fulltext-only fallback from a healthy hybrid result. (#20) - Degenerate embeddings caught at write time —
engrama/embeddings/health.is_degenerate_vectorflagsneeds_reindex=trueon the node and skips vector storage;list_nodes_for_embedding(force=False)now pulls those nodes back soengrama reindexheals them. (#21) under_connectedreflect pattern excludes stub neighbours — both backends now filter outstatus='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 theGraphStoreandVectorStoreprotocols on top ofsqlite3and thesqlite-vecextension. Modules:store.py(sync graph store, 36+ methods, FTS5 fulltext),async_store.py(async wrapper that mirrorsNeo4jAsyncStore's rich return shapes),vector.py(SqliteVecStoreusing thevec0virtual table),schema.sql(auto-applied on first connect). Default DB path:~/.engrama/engrama.db(override viaENGRAMA_DB_PATH). - OpenAI-compatible embedding provider (
engrama/embeddings/openai_compat.py) — single client speaking the OpenAI/v1/embeddingsshape, 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()andcreate_async_stores()dispatch the engine, CLI, SDK, and MCP server through a single entry point.GRAPH_BACKENDenv var (or explicit config dict) selects the implementation. Skills and tools no longer hardcode any backend. engrama-mcpCLI flags —--backend {sqlite,neo4j}(defaultsqlite),--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 oversqlite-asyncandneo4j-async. Pins the rich response shapes the MCP server depends on (merge_nodereturns{"node": ..., "created": ...}, neighbours come back as{label, name, via, properties}, etc.). 12 tests × 2 backends. get_approved_titleson 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.pySQLite branch) — pass on a fresh checkout with no.envand 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_BACKENDis nowsqlite(wasneo4j). Existing installs that rely on Neo4j must setGRAPH_BACKEND=neo4jexplicitly. neo4jdriver moves to an opt-in extra (uv sync --extra neo4j, or theengrama[neo4j]extra once Engrama ships on PyPI). Base install ships withsqlite-vec,httpx,pydantic,python-dotenv,pyyamlonly.Neo4jGraphStorereturns plain dicts at the boundary (Phase 1 of the spec).EngramaEngine,recall.pyand other internal callers consume Pythondictrather than driver-specific types.HybridSearchEnginecopies enrichment fields (summary,tags,confidence,updated_at) ontoSearchResultfor vector-only hits. Both backends'search_similarnow project these fields.engrama initis a no-op on SQLite for Cypher schema statements (the schema is inbackends/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.examplenow defaults to the zero-dep SQLite path with Neo4j commented as opt-in.
Fixed¶
- Async store contract drift on SQLite.
SqliteAsyncStorewas forwarding sync calls via__getattr__, leaking the legacy[{"n": ...}]shape to the MCP server which expected the richNeo4jAsyncStore-style{"node": ..., "created": ...}. Crashed anyengrama_remembercall 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 inengrama_reflectcalledmerge_nodewithstatus="pending", silently undoing user approvals on re-runs. Reflect now filters candidates againstdismissed | approved. The output payload also exposesapproved_countnext todismissed_count. - Search dropping enrichment on pure-semantic hits. The hybrid
scorer only copied
summary/tagsfrom fulltext results, so nodes ranked solely by vector similarity surfaced with empty fields.search_similarnow 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
neo4jextra; 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()inNeo4jAsyncStoreapplies 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 inNeo4jGraphStore.decay_scores(). valid_tosupport:merge_node()now acceptsvalid_toto mark facts as superseded. Settingvalid_toauto-halves confidence. Updating a superseded node clearsvalid_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-runnow shows a sample table of nodes that would be affected with current vs projected confidence values. - Enhanced stale knowledge detection:
reflectstale_knowledge pattern now also considers nodes withconfidence < 0.3as 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 totest_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 Neo4jDateTimeobjects via.to_native().search.py: min-max normalization returns1.0for single-result sets (was0.0).
Fixed¶
- Search normalization bug: a single search result normalized to score 0.0 instead of 1.0.
- Neo4j
DateTimeincompatibility indays_since():TypeErrorwhen subtractingneo4j.time.DateTimefromdatetime.datetime. valid_toand caller-suppliedvalid_fromnow stored as Neo4jdatetime()instead of raw strings, fixingquery_at_datecomparisons.
[0.7.0] — 2026-04-13¶
Async embedding providers and hybrid search (DDR-003 Phase B + C).
Added¶
- Async embedding methods:
OllamaProvidernow hasaembed(),aembed_batch(),ahealth_check(),aclose()usinghttpx.AsyncClient. Sync methods (urllib) remain for CLI/SDK backward compatibility.NullProvideralso has async counterparts. - Async hybrid search:
HybridSearchEngine.asearch()— async counterpart ofsearch(). Usesaembed()and async store methods. Deployed in MCP server for non-blocking search. - Embed-on-write:
engrama_rememberandengrama_sync_noteMCP tools now embed nodes automatically whenEMBEDDING_PROVIDERis configured. Uses asyncaembed()to avoid blocking the event loop. core/text.py— re-export ofembeddings/text.pyfor import convenience.httpx>=0.27added as optional dependency (embeddingsandmcpextras).- 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 totest_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
GraphStoreandVectorStoreforHybridSearchEngine.asearch(). - MCP
engrama_search: usesHybridSearchEngine.asearch()with the async store directly, eliminating the sync→async impedance mismatch. - MCP embed-on-write: uses
await embedder.aembed()instead of syncembedder.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,EmbeddingProviderprotocols incore/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 inserver.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 inbackends/__init__.py— readsEMBEDDING_DIMENSIONSfrom config/env, returns configuredNeo4jAsyncStore.count_labels()andclose()methods on syncNeo4jGraphStore.NullGraphStoreandNullVectorStore— 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) andtest_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) andHYBRID_GRAPH_BETA(graph topology boost).
Fixed¶
- BUG-006:
engrama_searchreturnednullnames for title-keyed nodes (Decision, Problem, Vulnerability, etc.). Fulltext search and neighbour queries now useCOALESCE(node.name, node.title). - BUG-007:
engrama_reflectgenerated duplicate under-connected Insights on repeated runs._detect_under_connectednow checks for existing pending/approved Insight bysource_querybefore creating; updates in place if found; respects previously dismissed Insights. - BUG-008:
engrama_contextshowed duplicate relation entries in theviaarray. Deduplicated inget_node_with_neighbours.
Changed¶
- server.py: rewired from inline Cypher to
Neo4jAsyncStoremethod 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
Neo4jAsyncStoreviacreate_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_ingestMCP tool. Reads a vault note, raw text, or conversation transcript and returns content with entity extraction guidance. The agent extracts entities and callsengrama_rememberfor each one. Includes graph deduplication hints (existing nodes listed in response). - Phase 2 — Adaptive Reflect:
engrama_reflectnow 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_remembercalls. After 10+ entities stored since last reflect,engrama_rememberreturns aproactive_hintsuggesting the agent run reflect.engrama_searchchecks for pending Insights related to the search query and surfaces them inline.engrama_reflectresets 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_statemoved from FastMCP lifespan context (not reliably mutable across tool calls) to a module-level dict. Counter now persists correctly acrossengrama_rememberinvocations. - training_opportunity never activating: query only matched
Problem {status: "open"}but real graphs haveVulnerabilitynodes (status: "demonstrated"). Broadened WHERE clause:(issue:Vulnerability) OR (issue:Problem AND issue.status = $open_status). - shared_technology skipped in most graphs: required both
ProjectANDTechnologylabels, but many graphs have Courses or Decisions sharing technologies. Broadened: matches any entity viaUSES/TEACHES/COMPOSED_OF, activation requires onlyTechnologylabel. - stale_knowledge skipped when only Courses exist: activation required
Projectbut the query also checksCourseconnections. Broadened: activates when eitherProjectORCourseexists. _run_patterntoo rigid for OR-logic: addedany_labelsparameter — each entry is an OR-group where at least one label must have data. Used bytraining_opportunity(Problem OR Vulnerability + Course) andstale_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_searchresponse now wraps results in{"results": [...]}object (was bare array) to accommodate optionalpending_insightsandproactive_hintfields.- 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
initdropped fulltext index — comment lines inside;-split chunks caused the entire CREATE FULLTEXT INDEX statement to be silently discarded. - BUG-002:
engrama_remembernever created vault notes — nodes had noobsidian_path, breaking the DDR-002 contract. Now creates full YAML frontmatter with engrama_id, type, properties, and empty relations block. - BUG-003:
engrama_relatefailed to write to vault becauseobsidian_pathwas always null (cascading from BUG-002). Added fallback: if source node has no vault note, create one on-the-fly. - BUG-005:
engrama_remembercrashed whenrelationsdict was passed insideproperties— 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 initnow seeds domain nodes and key concepts for hacking, teaching, photography, and AI modules. - BUG-005: Inline relations in
engrama_remember— passrelations: {TEACHES: [Python]}and targets are found/created (with stub creation) + relationships merged + vault frontmatter written, all in one call. - FIX-008:
Materialnode 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
remembernow supports inline relations. - FIX-007: System prompt section 3 —
INSTANCE_OFis 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.3tov0.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
relationsmap. Vault and graph are co-equal sources of truth. ObsidianAdapter.add_relation(),remove_relation(),set_relations()— idempotent frontmatter relation management.NoteParserextractsrelationsfrom 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.AssociateSkillwrites 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, generatesschema.pyandinit-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:
Engramaclass wrapping all skills. - CLI:
engrama init,engrama verify,engrama reflect,engrama search. - 100 integration tests across 9 test files.