Special Thanks
- NotAFlightRisk for reporting GHSA-wpp8-2qcw-x4pc, the missing scope check on MCP
prompts/get. - manus-pi for reporting GHSA-hm53-3f6c-cm5h, host details exposed by
/api/server/status. - massimiliano1991 for the Milvus store-confinement work (#1879).
- GPT-AGI for the recall-range report ("from ... to ..." kept only the start).
Highlights
- Security.
prompts/getnow requires a globalreadscope, so a store-restricted token can no longer read other stores through prompts (GHSA-wpp8-2qcw-x4pc)./api/server/statusreturns process ID, OS, kernel and Python version to admin callers only (GHSA-hm53-3f6c-cm5h). - Ontology overlay. REST API under
/api/ontology, a dashboard section in the Manage tab, and sync of overlay writes between sqlite_vec nodes through the hub (#1897, phases 2 and 5). - Dashboard. Tag management gets an adjustable minimum-use filter; the disabled System Maintenance placeholders are removed.
- Search. A "from X to Y" recall range now covers the whole window.
Changelog
Added
- Add an adjustable minimum-use filter to dashboard tag management, defaulting to tags used at least twice.
- REST API and dashboard section for the ontology overlay (#1897, phase 5).
GET /api/ontologyreturns the merged memory types, relationships, type patterns and
retention periods, plus the stored overlay rows.POST/PUT/DELETEroutes under
/api/ontology/add and remove memory types, retention periods, relationship types and
type patterns through the same storage methods, so writes are validated and synced the
same way. Writes need the globalwritescope, since the ontology is shared by every
store. The dashboard's Manage tab gets an Ontology section to add memory types, set
retention periods and remove stored entries. - The ontology overlay syncs between sqlite_vec nodes through the hub (#1897, phase 2).
WithMCP_SYNC_EVENTLOGon, every overlay write (memory types, retention, relationship
types, type patterns) records anontology_upsertorontology_deletesync event keyed
ontology:<kind>:<key>. Peers resolve them per key by HLC, as they do memory events, and
write the winning row./api/sync/capabilitiesadvertisesontology_sync; the feed
(GET /api/sync/events?events=memory|ontology|all), push and baseline carry ontology events
only to a peer that advertises it, so an older peer keeps syncing memories without
stalling on an op it does not know. The first cycle with a capable peer exchanges the
ontology events both cursors had already passed. If that exchange fails, the peer's
cycle shows asincompleteand the exchange is retried, while memories keep syncing.
Malformed ontology events are skipped, not retried.
Fixed
- Delta sync restores the recreated memory's fields before merging later metadata edits, regardless of delivery order.
- A "from … to …" recall range kept only the start (GPT-AGI).
from January to Marchandfrom 2024-01-01 to 2024-03-31were read as the first month or the first day. They now cover the whole window, the same waybetween … and …already did. A from/to phrase that is not a date still falls through, sofrom Alice to Bob yesterdaystays yesterday. A month name with a year keeps that year (from January 2024 to March 2024),to nowends today, and a window whose start comes after its end (from march to january) keeps the previous single-month reading. - Dashboard update failed in pip-less uv environments after
git pullhad already moved the checkout (#1943).
/api/server/updatenow resolves the installer for the running interpreter beforegit pull. It uses
python -m pipwhen pip is importable. Otherwise it usesuv pip --python <interpreter>, taking the uv that
launched the server ($UV) or else uv on PATH. If neither is available, it refuses with a clear error and
leaves the checkout unchanged. - Dashboard logged
THREE.WARNING: Multiple instances of Three.js being importedon every load (#1944).
The import map pointedthree/webgpuat esm.sh's bundled build, which embeds its own copy of
three's core next to the onethreeloads. It now mapsthree/webgputo the unbundled build and
maps thethree/build/three.core.jsspecifier it imports to the same modulethreeuses, so one
core is loaded. The import map's CSP hash inweb/app.pyis updated to match. - Cloudflare tag deletes treated
%and_in a tag as wildcards and deleted memories that did not have the tag (#1948).
delete_by_tags,delete_by_timeframeanddelete_before_datebuilt their D1
tags LIKE ?patterns from the raw tag, so deleting the tag%deleted every memory
with two or more tags, andsnake_casealso deleted a memory taggedsnakeXcase. The
patterns are escaped now and carryESCAPE '\', as the sqlite-vec deletes and the
Cloudflare exact-content search already do. - The
cascade/llmNLI backends no longer hide their heuristic fallback.
With no LLM provider configured, or on an unparseable/failed LLM response,
the classifier silently returned heuristic verdicts whiledegraded
stayedfalse— a harness or quarantine report then read as a clean LLM
run. All three fallback paths now setdegraded, matching the
transformers backend. - Milvus updates no longer embed the content between the read and the upsert
(#1966).update_memory_metadata(and soupdate_memory) and
update_memories_batchread the stored vector with the row and write it back,
asmark_superseded_batchalready did; only a row whose vector does not come
back is re-embedded.update_memory_versionedembeds the new content before it
reads the old row. The window between the read and the whole-row upsert is now
two round-trips with no embedding call in it; it is not closed, since pymilvus
has no conditional upsert, and that limit stays documented. - Avoid false sentence breaks at content chunk-window edges (#1975). A truncated
window no longer treats its artificial end as punctuation; a fitting URL, version,
or decimal remains whole when the next real boundary is a word break. - mcpmemory.services told readers to install without the embedding runtime, and hid its navigation on phones (#1983).
The home page, the browser demo, the remote MCP tutorial and the Open Graph card showed
pip install mcp-memory-service, which has no ONNX runtime, so semantic search falls back
to hash embeddings. They now showpip install "mcp-memory-service[sqlite]", as the
language pages already did. Below 880px the main navigation wasdisplay:nonewith no
replacement; it wraps into its own row. The hero console was empty without JavaScript and
now has its first example in the HTML. "OAuth 2.0" is corrected to 2.1, "Cost: $0" and
"100% local" are reworded. The pages gain JSON-LD, the tutorial's canonical and sitemap
entry use the extensionless URL Cloudflare Pages redirects to, and the home page's first
load drops from 2,837 to 535 KiB because the loop video loads only while visible. - Milvus dropped the store a memory was written to and served it from any store (#1879, massimiliano1991).
The collection had nostorefield, sostore()andstore_batch()discarded the
value, andget_by_hash,update_memory_metadataanddeletefound a memory whatever
store the caller named. New collections get astorefield (default"default"), the
three hash-addressed calls are confined to it, and every upsert writes the row's own
store back, so an update or a versioned update never moves a memory to"default".
A collection created before this has no field to confine on: there a named store is
refused with one warning per instance (None,"all"and"default"are served as
before), and the collection has to be recreated to pick up confinement, as for
content_lower.get_by_hash_is_store_scopedstaysFalse: search and listing still
ignorestoreon this backend.
A confineddeletewhose filter removes nothing (the row was deleted, or re-stored
elsewhere, between the read and the delete) now reports "not found" and leaves the
access record and graph edges alone, instead of removing them and reporting success.
A scoped update is confined within one client only: it is not race-safe across clients
sharing a collection, since a row re-stored elsewhere while the update is in flight is
written back by the unconditional upsert (the cross-client guard is a separate issue).
update_memories_batchandmark_superseded_batchnow skip soft-deleted rows, as
get_by_hashdoes, instead of writing them back and counting them as updated. /api/server/statusno longer discloses host details below admin scope
(GHSA-hm53-3f6c-cm5h). Process ID, OS, kernel and Python version are now
returned to admin callers only; read-scoped and anonymous callers get
version, uptime and timestamp. The dashboard shows-for those fields
without admin scope.- MCP prompts refuse store-restricted tokens (GHSA-wpp8-2qcw-x4pc).
prompts/getapplied no scope check, so aread:<store>token could read
other stores' memories throughknowledge_exportandmemory_analysis
(tag search spans every store) and thedefaultstore through the other
prompts. Prompts now require a globalreadscope, like MCP resources.
Removed
- Dashboard: removed the System Maintenance placeholders (#1974). The Manage tab
showed "Optimize DB" and "Rebuild Index" buttons that were disabled and, behind them,
only answered "not yet implemented". The section, its handlers, its strings in all
eleven catalogs and theoptimize_db/rebuild_indexbranches of
POST /api/manage/system/{operation}are gone; the route now answers both with the
400 for an unknown operation.cleanup_duplicatesis unchanged.
Internal
quality/ai_evaluator.pylogs with lazy%-style arguments (#1146).
Its four multi-line f-string logger calls in the DeBERTa / MS-MARCO
fallback path now pass the scores (%.3f), thresholds and decision (%s)
as arguments. They were invisible to the line gate and only the AST scan
caught them. The module is added toGUARDED_MODULES.server/client_detection.pylogs with lazy%-style arguments (#1146).
When reading the parent process's command line fails, the error is now
passed through_sanitize_log_valueinstead of an f-string, so text from
another process cannot forge a log line. The module is added to
GUARDED_MODULES.utils/content_splitter.pylogs with lazy%-style arguments (#1146).
The two logger calls insplit_content()andvalidate_chunk_lengths()now
log integer character counts and chunk indices via%dinstead of f-strings,
and the module is added toGUARDED_MODULES.embeddings/external_api.pylogs with lazy%-style arguments (#1146).
Its two f-string logger calls now pass their values as arguments. The API
URL and model name, read fromMCP_EXTERNAL_EMBEDDING_URLand
MCP_EXTERNAL_EMBEDDING_MODEL, go through_sanitize_log_value; the
embedding dimension and batch size are logged with%d. The module is added
toGUARDED_MODULES, withapi_urlinEXTERNAL_NAMESandmodel_name
listed for this module only, so dropping either wrapper fails the test.ingestion/chunker.pylogs with lazy%-style arguments (#1146).
Its one f-string logger call now passes the chunk and character counts as
%darguments.chunker.pyand the already cleanregistry.pyjoin the
five loaders inGUARDED_MODULES, so every module iningestion/that logs
is now held by the log-injection ratchet.resolved_path, the registry's
canonical form of the caller's path, is added toEXTERNAL_NAMES.quality/metadata_codec.pylogs with lazy%-style arguments (#1146).
Its two f-string logger calls now pass their values as arguments: the part
count of a malformed quality-metadata CSV string with%d, and the decode
error through_sanitize_log_value. The module is added to
GUARDED_MODULES, so the log-injection ratchet keeps it clean.server_impl.pylogs with lazy%-style arguments (#1146).
Its four f-string logger calls each spanned lines, so the line gate never
saw them and only the AST scan did. The Milvus URI and collection name read
from the environment are wrapped in_sanitize_log_valueon both the eager
and the lazy init path; the caller-provided storage class name and the
unresolved-conflict count go as%sarguments. The module is added to
GUARDED_MODULES.api/sync_wrapper.pylogs with lazy%-style arguments (#1146).
When a wrapped call from the code-execution API fails, the exception is now
passed through_sanitize_log_valueinstead of an f-string, so error text
that echoes a caller's query or content cannot forge a log line. The module
is added toGUARDED_MODULES.utils/system_detection.pylogs with sanitised lazy%-style arguments (#1146).
The three exception logger calls in_get_system_memory(),_check_cuda_available(),
and_check_mps_available()now pass_sanitize_log_value(e)to%sinstead of
f-strings, and the module is added toGUARDED_MODULES.utils/cache_manager.pylogs with lazy%-style arguments (#1146).
Its f-string logger calls now pass their values as arguments. The backend
name, thebackend:pathcache key and the caller's context label go through
_sanitize_log_value, as the same cache lines inmcp_server.pyalready do;
call counts and storage IDs are logged with%d, init times with%.1f.
The module is added toGUARDED_MODULES, so the log-injection ratchet keeps
it clean.
The ratchet's lazy scan treatscontext_labelas outside data everywhere, and
backendin this module only (self.backendis internal in the PDF loader
andreasoning/nli.py), so dropping either wrapper fails the test.utils/debug.pylogs with lazy%-style arguments (#1146).
exact_match_retrieve(), which backs theexact_match_retrieveMCP tool,
logged the backend's exception through an f-string. A backend error that
echoed the caller's content could forge a second log line. The error now
goes through_sanitize_log_value, and the backend class name is a%s
argument. The module is added toGUARDED_MODULES.