Special Thanks
Thanks to the external contributors in this release: @ShahadatTest (Credentials tab requests, #1841), @massimiliano1991 (typed-edge gates, #1888), @mbreitkreuz (conflict tags and entity linking, #1872, #1889), @tonydzi (typed edges off by default, #1887), @qwist1233-cpu (untagged deletes and dashboard translations, #1871, #1822) and @ducanhnguyen223 (Codex short replies, #1716).
Highlights
- Behavior change:
MCP_TYPED_EDGES_ENABLEDnow defaults tofalse. Consolidation stores inferred associations asrelatedinstead offixes/contradicts. Set it totrueto keep the old behavior; the stricter #1457 gates apply then. - Store scoping is enforced in the storage layer for metadata updates, the
fallback=truesearch and beliefs. conflict:unresolvedno longer outlives the conflict, and entity linking no longer overwritescontradictsedges.- New opt-in
contextualCapturesetting for Codex short replies.
Changelog
Fixed
- Consolidation's typed-edge gates are the ones its docs state (#1457). The docs said
confidence 0.75 and similarity 0.65, while the code shipped 0.50 and 0.45 and consolidation
ran at 0.6 and 0.45. On three hand-checked stores neither setting made a label right
(0 of 139), but the #541 gates write about ten times fewer of them, so those are now the
constructor defaults and the consolidator passes them explicitly. The 0.65 is on the
normalized score(cos + 1) / 2, i.e. cosine 0.3, and the docstrings say so. These gates
apply only withMCP_TYPED_EDGES_ENABLED=true, off by default since #1887.
scripts/maintenance/update_graph_relationship_types.pybuilds the same engine, so it
picks up the 0.75; it passes no similarity, so the similarity gate does not apply there. - Optionally capture short Codex replies with their preceding assistant context (#1664). The new
contextualCapturesetting is off by default and stores an opted-in reply/response pair with source turn metadata, without an LLM call. - Search filter tooltips and most of the Documents tab stayed English (#1817).
The five filter tooltips and the three Documents help icons already had catalog
keys, but only ondata-i18n-aria, so the tooltip a user actually hovers kept its
Englishtitle;data-i18n-titlenow sits next to each aria attribute. Those icons
also sat inside the element whose text is translated, andapplyTranslations()
writestextContent/innerHTML, so the first render dropped them from the page --
the translatable text now lives in its own<span>and the icon stays beside it.
The upload history (No uploads yet, its error line), the View/Remove buttons of
both document lists, the drop zone thatapp.jsrebuilds after an upload, the
chunk-overlap illustration, the processing-mode help heading, the memory-viewer chunk
count and the removal confirmation go throughthis.t()or adata-i18n*attribute
now, with twenty-two new strings in all eleven catalogs. The chunk-size help icon also
gets its owninfo-icon-chunkclass: it shared the bare.info-iconhook that
setupDocumentsEventListeners()queries, so once the processing-mode icon stayed in
the page the wrong icon answered the click.documents.supportedalso lists DOCX and
PPTX, whichsemtools_loader.pyhas accepted all along, and the static#fileInput
now accepts.docx,.pptxlike the inputapp.jsrebuilds. - Test Connection and Save in the Settings Credentials tab send their requests again (#1839). Both called
apiCall()with a fetch-style options object instead of the method and the data as separate arguments, so neither request carried the method or body it needed.tests/web/test_credentials_apicall_args.pychecks both calls. conflict:unresolvedno longer outlives the conflict (#1840, mbreitkreuz). Superseding or deleting one side of a conflict (mark_superseded_batch,resolve_conflict,delete, a versioned update) left the tag on the other side, andresolve_conflictstripped it from the winner even while the winner still contradicted another memory.resolve_conflictalso refuses a memory resolved against itself now. The partner's tag is now cleared in the same transaction once it has no current conflict partner left, a new version no longer inherits the tag, and themaintainconflicts step reports (dry run, the default) or clears tags left behind by older data, bulk deletes and sync. sqlite-vec only.POST /api/manage/delete-untaggednow deletes through the storage layer on the SQLite backends instead of tombstoning the rows with a rawUPDATEon the shared connection. The newDeleteMixin.delete_untagged_memories()removes the graph edges, retracts the beliefs and summaries that cited the deleted hashes, appends one delete event per hash for delta-sync peers, and commits once under the connection lock — the same shape asdelete_by_tags().count_untagged_memories()joins it, so both endpoints now go through one predicate and the raw-SQL fallbacks are gone.- The
confirm_countthe caller is held to is now re-checked where the rows are selected for deletion — inside the connection lock and the transaction on SQLite, before the first write — so a concurrent request that strips the last tag off another memory between the count and the delete can no longer widen the delete past what was confirmed. The endpoint answers such a race with the same400as a stale count. - Korean landing page headline reworded (#1873). The hero read
AI 어시스턴트에게 오래 남는 기억을; it now readsAI 어시스턴트를 위한 기억력,
matching the page title. - Entity linking no longer replaces
contradictsedges (#1874, mbreitkreuz). WithMCP_ENTITY_LINKING_ENABLED=true, storing a memory that contradicts an existing one recorded the conflict and then overwrote bothcontradictsedges withshares_entity, because a pair holds one edge and the linker runs after conflict detection.memory_conflictsand themaintainauto-resolve lost the pair while both memories keptconflict:unresolved. The linker now skips any pair that already has an edge other thanhas_entity/shares_entity, the rulerebuild_entitiesalready used. Pairs overwritten before this fix are not repaired: if you ran with entity linking enabled, runmemory_qualitymaintainas a dry run first. An overwritten pair shows up as two reported hashes joined by ashares_entityedge; resolve it withmemory_resolvebefore runningmaintainwithdry_run=false, which clears the tag and with it the last trace of the conflict. - Store-scoped metadata updates are confined in the storage layer (#1875, part of #1683).
update_memory_metadataandupdate_memory_versionedlooked a memory up bycontent_hashalone. A claim on one store was only enforced by aget_by_hashpre-check in the MCP handler, so the check and the write were two separate steps. Both methods now takestore, putAND store = ?into the lookup and theUPDATE, and report a memory in another store as not found without changing it. A metadata update whoseUPDATEmatches no row, because the memory was deleted or moved between the read and the write, now returns not found instead of success, and a versioned update aborts if the old row leaves the store while the new version is written. The MCP handler andPUT /api/memories/{hash}pass the claimed store through.Noneand"all"stay unscoped, so callers without a store behave as before. sqlite-vec and hybrid only; the other backends still refuse store-scoped updates until #1878 and #1879 land. memory_searchwithfallback=truenow runs its exact and tag fallbacks in the same store as the main search. Before, both fallbacks searched thedefaultstore whateverstorethe call named, so a search in one partition could return another partition's memories. A per-store credential (read:<store>) can now make afallback=truesearch; it used to need the global grant.- Beliefs and the NLI on-store check respect the store (#1877, part of #1683). Beliefs aren't stored per store, so
memory_searchwithinclude_beliefsreturned beliefs from every store, and the opt-in NLI check on store (MCP_NLI_ON_STORE) compared a new memory with all of them. A per-store grant therefore could not coverinclude_beliefs. A belief is now visible in a store when none of its live source or contradicting memories sits in another store. Deleted memories don't count, so retention never hides a belief. The beliefs section of a search uses the same store as its memory results ("default"unless one is given), and the on-store check, in both the synchronous and the background path, compares a new memory only with beliefs visible in the store it was written to. A single-store user, with everything in"default", sees every belief and keeps challenging them as before.include_beliefsno longer needs the global grant. - Consolidation no longer labels inferred edges
fixes/contradictsby default (#1883).
MCP_TYPED_EDGES_ENABLEDnow defaults tofalse. A typed label came from the pair of
memory types (learning->buggavefixes, two note-like types gavecontradicts),
not from what the two memories say, and hand-checked samples from three stores found
0 of 139 typed edges correct. Inferred associations are still stored, asrelated.
This release does not relabel stored edges in bulk, butstore_association()writes
withINSERT OR REPLACE, so a pair that a later consolidation finds again is
stored asrelatedand loses its typed label.MCP_CONSOLIDATION_AUTO_SUPERSEDE
acts oncontradictsedges, so with the new default it has nothing to act on. To keep the old behavior, set
MCP_TYPED_EDGES_ENABLED=true; the #1457 gates still apply. - Keep selected source memories intact during semantic compression so rules retain
their exceptions and conditions, including short caveats and separate paragraphs.
Omit excerpts that exceed the summary budget instead of storing a partial rule.
Internal
pre_pr_check.shnow runs the changelog fragment check. Check 6.11 calls
scripts/ci/check_changelog_entry.sh --staged origin/main, the same script CI's
changelog.ymlruns, but against the index, so a missing fragment or one without a
-list item fails locally instead of in CI after the push.
Full changelog: https://github.com/doobidoo/mcp-memory-service/blob/main/CHANGELOG.md