Repository navigation
v0.22.0
What's New in v0.22.0
Security
- Room consolidation applies the privacy filter before the embedder (#1464) - the text written by the consolidation LLM was embedded first and filtered when it was stored, so with an
openaiorollamabackend an unmasked secret in that text reached the remote embedding provider, and the stored vector came from the unmasked text. The text is now validated and masked first (strict mode refuses it, and a refused record is never embedded), like every other write path. - A project-local
uteke.tomlcan no longer set the cycles that delete memories (#1534) -.uteke/uteke.tomlin a cloned repository could set[lifecycle](deprecated_ttl_days = 0,max_deprecate_percent = 100,auto_prune_enabled = true,soft_delete_only = false),[dream]thresholds, an[aging]age or the[maintenance]auto-dream switch, and the next lifecycle cycle oruteke agingrun applied them to the user's store. These four sections are now read from the globaluteke.tomlonly, like[privacy]and[server]; the project file's copy is ignored with a warning.[recall]and[tier]stay settable per project. Anyone who set lifecycle per project moves those keys to the global file, or setsUTEKE_TRUST_PROJECT_CONFIG=1to keep the old behaviour. - rustls-webpki 0.103.16 and webpki-roots 1.0.10 (RUSTSEC-2026-0382, RUSTSEC-2026-0381) - certificate verification could accept intermediates that were distrusted (emSign). Both sit in the TLS stack of the HTTP client (
uteke upgrade, the OpenAI and Ollama embedding backends, the extraction client, the CLI-to-server client over HTTPS). Lockfile only; no code change.
Added
uteke importrestores the vectors of imported documents, anduteke import --full(#1587) - a bundle fromuteke export --fullalready restored documents (slug, title, parent, content, chunk text) but no chunk vectors, so the documents were invisible to semantic search until a manual re-save.importnow regenerates the chunk vectors after the restore and reportschunks_reembedded;uteke repair --reembedalso re-embeds chunks without a vector (chunks_missing,chunks_reembedded,chunks_failed) when the backend was down during the import.--fullstates that the input must be such a bundle; any other file is an error before anything is stored. A structural bundle is still detected without the flag.- Room links travel with
uteke exportanduteke import(#1588) - each exported memory carries the ids of its rooms (rooms, absent when it is in none, so a store without rooms exports exactly as before).importlinks every memory to the rooms that exist in the target store, also for a duplicate of a memory that is already stored, and reportsroom_links(new links) androoms_missing(rooms named by the file that the target lacks) in the CLI, inPOST /importand in the JSON result. To fill a target whose rooms do not exist yet: create them, then import the same file again; nothing is copied. The link author is the memory's author, orimport. - Add an existing memory to a room without copying it (#1589) -
uteke room link <memory_id> <room> [--author],POST /room/memory({ memory_id, room, author? }, LAB tier) and the MCP tooluteke_room_link_memory. The id is a full UUID or an unambiguous prefix, the room must exist (404 otherwise), and linking twice is not an error (already_linked: true, the first link and its author are kept). Before, the only ways wererememberwith a room (a duplicate memory) orroom move, which fails for a memory with no link in the source room. - Benchmark receipts standard (#1468) -
benchmarks/RECEIPTS.md, a receipts template, a backfilled receipt for the v0.19.0 LongMemEval-S headline (manifest with commit, dataset sha256 and raw-scores sha256; summary recomputed bybenchmarks/scripts/score_longmemeval.py; scope of the metric), a README link, andbenchmarks/scripts/check_receipts.py, run by the docs CI job, which fails when the README headline has no receipt. - The privacy filter now covers tags, metadata strings and
source(#1464, part 2c) - a secret in a tag, in any string inside the metadata or in thesourceis masked (or refused in strict mode) onremember, room remember,update,import,set_sourceand room consolidation, and counted inredactions.update_memory_detailedreturns what an update masked;PUT /memoryreturnsredactedanduteke_updatereports it. Importing a structural export (uteke export --full) restores a backup as it was and is not filtered. - Privacy filter reporting in the CLI and MCP (#1464, part 2b-2c) -
uteke rememberprints which kinds of secret were masked and its--jsonoutput gainsredacted;uteke importreports masked and rejected rows;uteke_rememberover MCP says so in its result, and in strict modeuteke_rememberanduteke_updatefail with a tool error that starts withsecret_detected:and names the kinds, never the value. [privacy]inuteke.toml(#1464, part 2b-2a) -mode(redact, the default,strictoroff) andallowlist(regexes) now configure the secret filter in the CLI,uteke-serveanduteke-mcp;UTEKE_PRIVACY_MODEoverrides the mode. The section is read from the global file only (a project file could otherwise switch the filter off), and a bad value falls back to the default instead of disabling it.redactedon the HTTP write responses andsecret_detectedin strict mode (#1464, part 2b-2b) -POST /rememberandPOST /room/rememberreturnredacted: truewhen the privacy filter masked a secret; a strict-mode write is refused with400 {"error": "secret_detected", "message", "kinds"}(also onPUT /memory);POST /importreportsredactedandrejected_secret. Additive; agreed with cloud in uteke-cloud#218.authoron every write path (#1427, phase 2) - a memory records who wrote it (the agent, user or tool name), with or without a room.POST /remember, MCPuteke_rememberanduteke remember --authoraccept it (free text, trimmed, 1 to 128 characters; an invalid value is a 400, a tool error or a CLI error before anything is stored). A room write stamps the named writer on the memory as well as on the room link; theanonymous/userfallback of the room link is not an author.authoris returned on the memory object by recall, search, list and get (omitted when unset), echoed byPOST /remember, and kept by export and import. Stores migrate to schema v22 on first open (see Internal below): back up a production store first. Filtering by author is not part of this phase. Contract: uteke-cloud#219 (cloud already has the field).
Fixed
-
uteke importworks on a store with no embedding backend (#1166) - the import asked the embedder for every row and failed on the first one when no backend was configured (open_with_backend(.., None)), unlikeremember, which stores such a row keyword-only. The import now does the same: no vector and no duplicate check, the rows are found by keyword search, anduteke repair --reembedadds the vectors once a backend exists. -
A namespace called
allno longer shares recall-cache entries with queries across all namespaces - the recall cache keyed an all-namespaces query as the namespace"all", so a cached cross-namespace result could be returned to a caller scoped to a namespace namedall(and the reverse). The key is now the namespace itself, absent for an all-namespaces query. Found by the CI review of the #1519 move. -
uteke-mcphonours[embedding]and[vector]and theUTEKE_EMBEDDING_*variables (#1535, #1537 step 2) - the MCP binary opened the store with the built-in ONNX backend and default settings, so withbackend = "openai"or"ollama"(or a customdims) configured it silently embedded with ONNX while the CLI and the server used the configured backend, writing vectors of another model into the same index. It now resolves the embedder with the same single resolution as the CLI and the server (uteke_core::embedding_config):[embedding]and[vector]from the global file, the project file with its untrusted keys stripped,UTEKE_EMBEDDING_BACKEND(trimmed, blank = unset) and the otherUTEKE_EMBEDDING_*variables over the file. An unreachable backend is reported in the tool result (Embedding NOT written: ...) instead of silently using ONNX, and an unsupported backend is a config error at start. With nothing configured nothing changes. Agreed with cloud in uteke-cloud#254 (cloud has a fixed server-side embedder; no objection). Checked with binaries fromdevelopand from the branch on the same configs. -
The CLI no longer sends the ONNX model name to an
openaiorollamaendpoint (#1537, step 1) - with[embedding] backend = "openai"and nomodel,utekesentmodel: "embeddinggemma-q4"to the endpoint (a real OpenAI rejects it) whileuteke-serveon the same file sent the backend's default,text-embedding-3-small. The CLI'smodelnow defaults to empty, like the server's, so an unset model is the backend's default everywhere (same withUTEKE_EMBEDDING_BACKENDset in the environment). Checked with binaries fromdevelopand from the branch against a mock embeddings endpoint. -
The embedding cache no longer serves the vector of another model (#1536) - the persistent cache (
embed_cache.db) was keyed by the embedder's constant name (openai,ollama,embeddinggemma-q4), so after changing[embedding] model,dimsor the endpoint, an already cached text came back with the old model's vector, and a vector produced by the cloud fallback was cached under the primary's name. The key is now an identity the embedder owns (backend, model, dimensions, and foropenaiandollamaa hash of the endpoint; the API key is not part of it), a cached vector whose length is not the embedder's dimensions is ignored, an empty vector is not cached, and the fallback wraps the cached primary so cloud vectors never enter the cache (with a fallback configured, a repeated text whose primary embedding fails now calls the cloud each time instead of being served from the cache). Rows written under the old key are deleted once when the cache is first opened (PRAGMA user_version1); the first embeddings after the upgrade are recomputed.Embeddergains a provided methodidentity()(defaultname:dims), so existing implementations keep compiling. -
UTEKE_HOMEpointing at another store no longer routes to a running server (#1445) - with[server] enabled = trueanduteke-serverunning,UTEKE_HOME=/tmp/iso uteke remember ...was sent to the server and landed in the server's store instead of/tmp/iso. The server now reports which store it serves on the newGET /store(LAB route, token required when auth is on):{"id": "<16 hex digits>"}, a SHA-256 prefix of the store's canonical path, never the path itself. Before routing to a loopback server the CLI compares that id with the store it resolved and runs locally on a mismatch (with a warning on stderr). The probe sendsUTEKE_AUTH_TOKENlike any routed command; without it an auth-enabled server refuses the command with 401, so nothing lands in the wrong store. A server without the route (404), an unreachable one or a non-loopback one keeps the previous routing.docs/core-contract.jsongains/storein thelablist only.
Changed
uteke-serveembeds a recall or a remember outside the store lock (#1518) - the whole request ran under one lock, so the embedding (a network round trip for anopenaiorollamabackend) queued every request behind it: throughput stayed flat however many clients there were. The router now embeds thequeryofPOST /recalland thecontentofPOST /rememberfirst and the call under the lock finds the vector in the embedding cache. The embedder slot lock is also released before an embed call. Measured withbenchmarks/internal/scripts/serve_load.pyagainst a 50 ms mock embedding endpoint: 8 clients went from 19 to 141 operations per second and the median latency from 420 to 59 ms; a single client is unchanged. No response changes. It applies once the first request has built the embedder, and only when the embedding cache is on (the default). The early embedding sends only what the real call would: a content goes through the same validation and privacy filter as a write (a secret is masked, a strict-mode refusal or invalid content sends nothing), and JSON content is left to the real call.- One remember sequence with one failure policy; MCP
uteke_remembergainssource,source_typeandauthor_type(#1516) -Uteke::run_rememberruns the whole write: the strict-privacy check of thesource, the choice between the room, contradiction, typed and plain entry points, and the provenance follow-ups (source,author_type,author). HTTP, MCP and the CLI only decode and render. A provenance value is validated before the write, so a follow-up can only fail on a database error; the memory is then stored, the call succeeds and the failure is a warning (MCP text line, CLI stderr, log) instead of an abort on the CLI and MCP and a 400 on HTTP, which would invite a duplicate retry. A--detect-contradictionwrite without a type now infers it like every other path (it storedfact).POST /room/rememberis a separate operation and keeps its own request. uteke remember --typeis honoured on a local store and no longer defaults tofact(#1439, PR E2) - on a local store the flag was ignored unless--roomor--detect-contradictionwas set (the memory got the inferred type), and via a server the defaultfactwas sent explicitly, so inference never ran. Now an explicit--typeis stored (and copied into the metadata, as onPOST /remember), and an omitted one is inferred from the content on every path. The local path validates content, tags and type with the same decode as HTTP and MCP.- The
RecallPolicy,ListPolicy,RememberPolicyandForgetPolicytypes are gone (#1439, PR G) - with every surface following the HTTP rules the policies had one value each.RecallRequest::decode,ListRequest::decode,RememberRequest::decodeandForgetRequest::decodetake only the input,ForgetRequest::resolveandresolve_memory_idno longer take a policy, andIdCharset,FullIdFormandUnmatchedPrefixTextare removed. No behaviour change; a library user ofuteke-coredrops the policy argument. - MCP
uteke_forgetand every MCP id argument follow the HTTP rules;uteke forgetis the same on a local store and via a server (#1439, PR F) - an unknown prefix isMemory not found: {id}and a full UUID that is not stored is refused too (it was passed to the core, which reported success; this also coversuteke_get/update/pin/supersede, which share the resolver). A full UUID is normalised to hyphenated lowercase on every surface, soPOST /forget?id=now accepts an upper-case or dash-less spelling of a stored id (it was a 404).uteke_forgetrefuses an argument it does not declare (tag,namespace).uteke forget <id>on a local store resolves an id prefix and refuses an id that is not stored (it printed "forgotten" for any string). Via a server,uteke forgetnow asks for the same confirmation as locally (the y/N prompt, or--confirm; a tag forget without--confirmprints the notice and deletes nothing), and--cold/--allare an explicit error instead of the misleading "Provide an ID" message. - MCP
uteke_rememberfollows the HTTP rules, and the CLI builds the same metadata on every path (#1439, PR E) - content and tags are validated before the write, an omittedtypeis inferred from the content (it used to be stored asfactwithout inference), an unknowntypeis refused and a giventypeis copied into the memory's metadata as onPOST /remember. Atagsthat is not an array of strings, atypethat is not a string and any argument the tool does not declare are now errors (they were silently dropped).roomstays MCP-only. In the CLI,--metais parsed once for both a local store and a server (numbers, booleans,rel:type:targetand a bare key were only understood locally);uteke remember --timestampnow works via a server (the anchor is applied before the request) and--roomvia a server is an explicit error instead of an "unknown field" rejection from the server. - MCP
uteke_listfollows the HTTP rules, anduteke listno longer drops filters via a server (#1439, PR D part 2) - the choice between a plain page, a point-in-time page and the pagination envelope moved out of the HTTP handler intoUteke::run_list(uteke_core::list_exec), used byPOST /listand the MCP tool (HTTP behaviour unchanged). Over MCP the tool now acceptsat(RFC3339, lists the memories that existed then) andinclude_meta(appends-- total N, has_more B, next_offset Nto the page), a field of the wrong type (limit,offset,tag,namespace,at,include_meta) is an error instead of a silent default, and an argument the tool does not declare is an error that lists the supported ones.uteke list --entityor--categorywith a running server used to be sent without the filter (the/listendpoint has none) and returned unfiltered rows; it now falls back to the local store, which applies them. - The default
limitis 10 for recall, search and list on every surface (#1439, PR D part 1) - the CORE standard confirmed by the owner on 2026-10-13 (uteke-cloud#173; cloud already implements it). Recall went from 5 to 10 (HTTP, MCP anduteke recall), list from 5 over HTTP and from 20 over MCP and the CLI, to 10 everywhere; search was already 10. List now has the same cap of 1000 on every surface (MCP and the CLI had none), and an MCPlimitabove 1000 is capped. Scripts that relied on the old defaults must pass--limitorlimit; the LongMemEval harness passes--limit 50and is unaffected. The MCP tool schemas anddocs/cli-reference.mdstate the new defaults and caps. uteke recall(local store) runs the same checks as HTTP and MCP (#1439, PR C part 3) - a blank or whitespace-only query is refused (it was accepted and returned whatever the strategy gave),--limitabove 100 is capped at 100 with a warning on stderr (there was no cap locally; the server and MCP already capped), and--atmust be RFC3339 with the shared error text. Decoding only validates: which core function runs, the strategies, the fusion weights and every default are untouched, so--limit 50as the LongMemEval harness uses it behaves exactly as before.- MCP
uteke_recallhonours the same parameters as HTTP and refuses unknown ones (#1439, PR C part 2) - the decision tree that turns a decoded recall request into core calls (explain, pack, unified recall, time travel, the memory-only path with its entity/category and before/after post-filters) moved out of the HTTP handler intoUteke::run_recall(uteke_core::recall_exec), and bothPOST /recalland the MCP tool call it. HTTP behaviour is unchanged. Over MCP the tool now acceptsentity,category,strict,enrich,at,afterandbefore(it ignored them before) with the same guards as HTTP (packandexplaincannot be combined with the time filters,explainnot with entity/category/enrich), and an argument the tool does not declare is an error that lists the supported ones (a typo such aslimtused to be ignored silently); the list is built from the tool's own schema. The MCP default stays memories and documents. - MCP
uteke_recallfollows the HTTP rules (#1439, PR C part 1) - the same arguments now behave the same onPOST /recalland over MCP:limitis capped at 100 (MCP had no cap), a blank or whitespace-onlyqueryis an error (MCP accepted it),tags: []means no filter (MCP filtered on nothing, so it returned no results), an invalidlimit(a string, a negative or a fractional number) is an error instead of silently becoming 5, andexplainwith anytypeis refused as on HTTP (explain is memory-only: omit type; MCP used to allowtype: "memory"). The tool description says so. Agreed with cloud in uteke-cloud#173; cloud already clampslimitto 1..100. The retrieval path, the CLI and the HTTP behaviour are unchanged; the entity/category/enrich/at/after/before parameters and the rejection of unknown arguments are in the next entry. - Internal: a performance baseline you can run per commit (#1468, groundwork for #1522 and #1518):
benchmarks/internal/scripts/serve_load.py(concurrent load onuteke-serve, p50/p95/p99 per operation) andmodal_perf.py(builds an exact commit on Modal, runsuteke benchand the load at several client counts, and writes a result file with a manifest).hot_path_bench(ignored tests inuteke-core) times the Rust and SQLite hot paths without the embedder, and its numbers show the micro-optimisations listed in #1522 are below 4% of a request. The first receipt, foraa75441, is inbenchmarks/internal/results/: throughput stays flat from 1 to 16 clients while p50 latency grows from 20 ms to 301 ms, the signature of the server's global request lock. - Internal: design notes for #1465 (
uteke connect), #1466 (raw capture), #1469 (sliced maintenance), #1470 (DX bundle) and the next steps of #1427, indocs/plans/. Documentation only. - Internal:
rusqlite0.41.0 (withlibsqlite3-sys0.39.1, bundled SQLite), replacing the dependabot PR #1551.Statement::column_namesnow returns an iterator, so the two column-existence checks inmemory/schema.rsno longer call.iter(); no behaviour change. - Internal:
operations.rsis split by concern, #1519 part 1 (round 2 of the architecture deepening, #1523). It is now the module directoryoperations/; the lookup (get_by_id,get_by_ids,resolve_id_prefix, counts), tags, update and point-in-time methods moved verbatim to their own files, as didmemory_existed_at. A pure move: no public path, signature or behaviour changed, and the test count is unchanged (more parts follow). Part 2 moves forgetting (forget,soft_forget,promote, the bulk variants and their tests) and listing with namespace management (list,get, the namespace counts,move_memory,rename_namespace,delete_namespaceand their tests). Part 3 moves writing (remember*, the embedding step,commit_memory, the dedup check) and recall and search (recall*,search, the fusion and boost helpers) with their tests;operations/mod.rskeeps the shared types and the embedding retry. This completes the split. - An update with the same content does not re-embed (#1532) -
uteke update,PUT /memoryanduteke_updatewith acontentequal to the stored text used to compute and write a new vector anyway; the text is now treated as unchanged (after the privacy filter), so only the other fields are updated and the embedder is not called. An update already kept the memory id andcreated_atand never went through the dedup check ofremember; tests now cover both. room_deletesays what it did (#1531) - the result on the MCP tool, the CLI and the HTTPnotenow say the room was unregistered, that its memories are retained (not deleted) and how to delete a memory (uteke_forget/uteke forget <id>/POST /forget), with the number of memory links removed. The MCP tool description says the same. The HTTP response fields (deleted,unlinked_memories) are unchanged; only the free-textnotechanged.- A store opened with a backend of another vector width is refused at open (#1540, part of #1543) - the dimension is now decided once, when the store is opened: what the backend (or the supplied embedder) produces against the vectors already stored in SQLite and the loaded index file. A mismatch is an error at open that names both widths and says how to continue (go back to the backend or
dimsthe store was built with, or rebuild withUTEKE_ALLOW_DIM_MISMATCH=1 uteke repair), instead of a late error at the first embedding or, when the index was rebuilt from SQLite, an obscure failure of the open. The check runs before the index is touched, so a refused open changes nothing.UTEKE_ALLOW_DIM_MISMATCH=1still skips it, as documented. Because the open is refused, read-only commands (list,stats) on such a store also need the variable until the store is repaired or the backend is switched back. A cloud fallback that returns vectors of another width than the primary is ignored instead of reaching the index (the primary's error is reported and the problem is logged). - Internal: constructing an embedder is separate from using it, #1539 (part of #1543).
embed/factory.rshas one function per backend (onnx,openai,ollama), the dimension check and the two decorators (persistent cache, cloud fallback), applied in one place and in one order byfactory::build;Uteke::ensure_embeddershrank from about 200 lines to the cached-init-error check, a call to the factory and the swap. The embedder lock is held only to look at the slot and to put the embedder in, and a separate build lock keeps two threads from loading the model twice. No behaviour change except one corner: a cloud fallback that cannot be built is now a warning and the primary runs alone from the first call (it used to be an error on the first call only, after which the primary ran alone). - Internal: one resolution of
[embedding]for the CLI and the server, #1537 step 1 (part of #1543).uteke_core::embedding_config::EmbeddingSectionis the section type both read, andEmbeddingSection::resolveis the only place that appliesUTEKE_EMBEDDING_BACKEND(trimmed, blank = unset), validates the backend and applies the otherUTEKE_EMBEDDING_*variables (EmbeddingSettings::with_overrides_from, which the core already used when it builds the embedder). The CLI's copy of the env precedence and of the supported-backend list, and the server's copy of the list and the validation, are gone. Visible differences: an unsupported backend gives the server's message (with the hint to setUTEKE_EMBEDDING_BACKENDor[embedding] backend) in the CLI too; a blankbackendin the file is now treated as unset by the CLI as it already was by the server (it used to be a config error); a non-numericUTEKE_EMBEDDING_DIMSwarns from the core for every consumer.[embedding] max_seq_lengthandUTEKE_MAX_SEQ_LENGTHhave never had any effect: the key is still accepted so existing files load, and setting either logs a warning. MCP is not changed (#1537 step 2). - Consolidation writes a memory like
rememberdoes (#1514, part 3) - a record written by room consolidation now goes through the same tail asremember: source hash, acreatedtimeline event, edge wiring (document references), recall-cache invalidation, and the cosine links, on top of the vector it already had. A vector that cannot be written leaves the record FTS5-only (as forremember) instead of rolling the row back; a failed room link still rolls it back. Thecreatedevent is visible inGET /timelineand needs the CORE contract text to say so (uteke-cloud#242, whose answer is that cloud follows). This completes the shared write path:remember, the contradiction path, import and consolidation now share one tail. - Import writes a memory like
rememberdoes (#1514, part 2) - an imported row now goes through the same tail asremember: source hash, acreatedtimeline event, edge wiring, recall-cache invalidation, and the vector and cosine links. Before, a recall cached before an import could be stale after it, and imported rows had nocreatedevent (visible inGET /timeline), no source hash and no auto-wired edges. A vector that cannot be WRITTEN (the index write) now leaves the row FTS5-only (as forremember;uteke repairbackfills it) instead of failing the whole import. A vector that cannot be GENERATED is different: a backend that is down or rate-limited still stops the import at that row, as before (the rows already imported stay, and importing the file again skips them through the duplicate check); a store with no backend at all imports keyword-only. The index is still saved once at the end. Consolidation insert follows in the next part; thecreatedevent on import needs the CORE contract text to say so (uteke-cloud#242). - Internal: the Cora Review job can use Anthropic workload identity federation (
.github/workflows/cora-review.yml, as in cora-code#700): whenANTHROPIC_FEDERATION_RULE_ID,ANTHROPIC_ORGANIZATION_ID,ANTHROPIC_SERVICE_ACCOUNT_IDandANTHROPIC_WORKSPACE_IDare set it exchanges its GitHub OIDC token for a short-lived token (sent only toapi.anthropic.com, modelANTHROPIC_MODEL, defaultclaude-opus-5-5); otherwise it falls back toCORA_API_KEY,CORA_BASE_URLandCORA_MODEL, and fails if those are missing. No product change. - A missing target is a 404 on every endpoint, and a 500 body is generic (#1517, part 2) -
PUT /memory(missing id),POST /room/rename(missing room),POST /room/memory/move(missing target room),POST /namespaces/rename(missing source) andPOST /consolidate/pair(missing id) answered 400 with aValidation error:prefix; they now answer 404 with the bare message (Memory not found: <id>,Room not found: <id>,Namespace not found: <ns>,memory <id> not found), like the other endpoints that name a thing. Other validation failures stay 400. A 500 from the endpoints that map core errors now carriesInternal server error(the detail is logged) instead of the error's own text; an embedding failure keeps its message because it says what to do (uteke repair). The core raisesError::NotFoundat these five sites, so MCP and CLI messages lose theValidation error:prefix too. Agreed with cloud in uteke-cloud#244 (cloud follows forroom/renameandconsolidate/pair). - Embedding calls go through one module, #1538 (part of #1543). Initialisation, the embedder lock and the retry now live in
embedding.rs(Uteke::embed,embed_once,embedder_max_seq_len); the ten call sites (recall, explain, document search, document chunks, room recall, update, import,embed_text, writes, reembed) ask it, and the fiveexpect("embedder ensured above")panics are gone. Behaviour change: query, document and room embedding now retry a transient failure like writes did (3 attempts, 200/400/800 ms) instead of failing at once;reembedtakes the lock per row instead of for the whole loop and still makes one attempt per row, so a repair over a dead backend reports each failed row without waiting. A write that cannot generate a vector is still stored FTS5-only with a warning; an embedder that cannot start is still an error. - Internal: tests can supply their own embedder through the public open path (#1542, part of #1543).
test_supportgainsopen_with_embedder,open_with_embedder_atand aRecordingEmbedder(records every text, can fail its first calls); the 33 core tests that were#[ignore]d becauseUteke::openneeds the ONNX runtime now run in CI with the deterministic keyword embedder (the document-chunk, wikilink-enrichment, room, dedup, fusion and verify/repair tests among them). No production code change. - MCP and the server read the engine settings like the CLI (#1515, part 4) -
uteke-mcpused to open the store with the built-in defaults and ignoreuteke.tomlfor recall, tier, graph rerank, dream, lifecycle and the embedding fallback. It now loadsEngineSettings(global file, project file with its untrusted keys stripped,UTEKE_RECALL_*/UTEKE_GRAPH_*/UTEKE_EMBED_FALLBACK_*) and applies it, anduteke_recalluses[recall] min_score,min_score_strictanddefault_strategywhen a call gives none, asPOST /recalldoes; a call's own floor or strategy still wins. MCP over HTTP (POST /mcp) gets the same from the server's settings.uteke-servenow also honours theUTEKE_RECALL_*andUTEKE_GRAPH_*variables (it ignored them before). With no[recall]section and no variable nothing changes. - Internal: one
EngineSettingsand one apply step, #1515 part 3 (round 2 of the architecture deepening, #1523).uteke_core::EngineSettingsholds[tier],[recall],[dream],[lifecycle]and[embed_fallback], andFallbackSettingsis deserializable like the others; the CLI config and the server config embed it with#[serde(flatten)]instead of writing the five sections twice, andUteke::apply_settingsreplaces the sequence of setters that the CLI and the server each repeated (the CLI no longer applies the fallback in a separate block; the server logs the dream and lifecycle settings on every start, not only when the section exists). MCP does not use it yet (next part). No behaviour change, checked by running the CLI and the server built fromdevelopand from this branch on the same config with all five sections, a partial file and an invalid one. - Internal:
[recall]is read into one core type, #1515 part 2 (round 2 of the architecture deepening, #1523).uteke_core::RecallSettingsowns the nine keys, their defaults and theUTEKE_RECALL_*/UTEKE_GRAPH_*overrides with their range checks (moved from the CLI, tested with an injected lookup); the CLI config uses it as its[recall]type and the server dropsRecallFileSectionand itsOptionchains. The server still does not read theUTEKE_RECALL_*variables: that is decided in the apply step of part 3. No behaviour change, checked by running the CLI and the server built fromdevelopand from this branch with the same[recall]section and environment. - Internal:
[tier],[dream]and[lifecycle]are read straight into the core types, #1515 part 1 (round 2 of the architecture deepening, #1523; the first step of closing #1355).uteke_core::{TierConfig, DreamConfig, LifecycleConfig}now implementserde::Deserialize(each key optional, the core default for the rest; negative tier days are still rejected), so the CLI config uses them as its section types and the server drops its threeOptionmirror structs and the field-by-field default chains.[recall]and[embed_fallback]follow in the next parts. No behaviour change. - Internal, API: transports no longer reach into the store, #1521 (round 2 of the architecture deepening, #1523).
Uteke::store()andUteke::graph_store()are no longer public (a breaking change for crates that depend onuteke-core);Utekeofferscount_active,list_deprecatedandgraph()(the knowledge graph) instead, andstore_for_tests()exists behind thetest-supportfeature for fixtures. The private module of theUtekeroom operations is nowroom_ops, so it no longer shares a name withmemory::rooms. No behaviour change. rememberwithdetect_contradictionwrites like plainremember(#1514, part 1) - the two now share one embedding step and one commit step. The contradiction path used to skip the memory type check (an unknown type was stored), embed JSON content as raw text instead of the flattened text, try the embedding once, and fail the whole write when the embedding failed. It now rejects an unknown type, embeds the flattened text, retries likeremember, and on a persistent embedding failure stores the memory FTS5-only with a warning (the contradiction check is skipped without a vector, as the duplicate check is). Import and consolidation move onto the same commit step in later parts, once uteke-cloud#242 is answered.- Internal: the redaction flag policy is its own module, #1520 (round 2 of the architecture deepening, #1523):
redaction_flags::resolvedecides whichredacted/redactionsflags a memory carries after an update as one pure function with a test per rule, andupdate_memory_detailedloses its inline five-armmatch. The three flag helpers moved with it and are still reachable asuteke_core::privacy::{with,without,carry}_redaction_flags. No behaviour change. - Internal: errors are classified once, part of #1517 (round 2 of the architecture deepening, #1523):
Error::NotFoundandError::class()(BadRequest,NotFound,SecretDetected,Internal), and onecore_errormapping in the server instead of seven hand-writtenValidation => 400, else 500arms. The two handlers that matchedcontains("not found")(POST /doc/move,DELETE /room/delete) now read the class. HTTP statuses do not change: a missing memory on update or a missing room on rename still answer 400 until the contract with uteke-cloud moves them. The one visible difference is the text of thePOST /doc/move404, which no longer starts withValidation error:. - Internal:
ort2.0.0-rc.12/rc.13 to rc.14 (replaces the dependabot PR, which cannot build): rc.14 builds sessions from an explicitEnvironment, so the one process-wide environment is now created inembed::ort_initand handed toSession::builder. The bundled ONNX Runtime stays at 1.24.4 (api-18). Checked with the real model:rememberwrites the embedding andrecallfinds it. - Internal: privacy filter modes and flag helpers, part 2b-1a of #1464 (
uteke_core::privacy):PrivacyMode(redact,strict,off),PrivacyConfig::prepare,Error::SecretDetectedand themetadata.redacted/redactionshelpers. Not wired to a write path yet. - Internal: privacy filter token rules and PEM blocks, part 2b-1a of #1464 (
uteke_core::privacy): Stripe,whsec_, Slack, Google, Basic andToken/Botauth,curl -u, password flags and URL passwords, and PEM private key blocks in their escaped, CRLF, collapsed, quoted and string-literal forms. Scan windows only bound the work. The shared vectors move to version 3 (113 cases so far). - Internal: the privacy filter is wired into the core write paths, part 2b-1 of #1464:
remember,remember_with_contradiction, room remember,update_memoryandimportmask secrets in the CONTENT before it is embedded, indexed or stored, so a masked secret is absent from the row, the FTS5 index, the timeline and the database file (checked by scanning the file bytes).Uteke::set_privacy_configtakesPrivacyMode(Redact, the default,StrictorOff) and the allowlist; a masked memory getsmetadata.redacted = trueandmetadata.redactions([{kind, count}], never a value), aredactedtimeline event, andRememberOutcome.redactions. The flags follow the content: an update to clean content drops them, a masked round trip or a metadata-only update keeps them.Strictreturns the newError::SecretDetected { kinds }(an import skips that row). Detection was reworked after two adversarial reviews (on 330 ordinary texts the first version masked 139, on 147 leaks it missed 62; now 3 and 16): a barekey,pass,tokenorsecretis an ordinary word and a name only when compound (API_KEY,DB_PASS,apiKey,PGPASSWORD) or written with=; a key or token value needs a digit, so type annotations and code references (token: &str,config.jwtSecret) are left alone; paths, ARNs, digests,0xaddresses, versions and dates are not secrets; quoted values, URL query strings, glued pairs (MODE=prod;API_KEY=...), escaped JSON, table cells and CLI flags (--password,docker login -p,curl -u) are read whole; new kinds: Basic andTokenauthorization headers, passwords in URLs, Stripe, Slack, Google, npm and webhook secrets; a private key block needs its matching END (or is cut off) and key-like content. Known limits, next parts: metadata, tags andsourceare not scanned, the structural import (uteke importof a full export), room consolidation and the LLM extraction paths do not call the filter yet, a secret glued to letters or_(MY_ghp_...), a quoted passphrase with spaces and a value containing(can still pass, a field namedpasswordholding a hyphenated phrase is masked, and nothing reads[privacy]fromuteke.toml, so until part 2b-2 the defaultRedactapplies to every write. - Internal: privacy filter assignment scanner, part 2b-1a of #1464 (
uteke_core::privacy): a secret-like name must end in a whole secret word (API_KEY,DB_PASS,apiKey; qualifiers such as_PRODare ignored), values are read whole (quotes, escaped JSON,:=,=>, table cells,&/;pair boundaries, passwords with,or\), and benign values (paths, ARNs, digests, versions, ULIDs, dates, emails, shell references, identifiers, placeholders) are left alone. On the 330-case benign and 147-case secret review corpus: 3 false positives, 16 misses (the first detector: 139 and 62). Shared vectors complete at version 3 (192 cases). A block of key lines is followed to its end up to 1 MiB. - Internal:
docs/privacy-filter-cases.json, the shared test vectors of the privacy filter (35 cases: every kind and 23 inputs that must stay untouched; inputs are split into fragments so no complete token appears, and the file is excluded from Cora review in.cora.yamlbecause its secret-shaped assignments are the point of the test).uteke-corepins every case, and uteke-cloud pins parity against the same file (uteke-cloud#218). Part 2a of #1464. - Internal:
memories.author, part 1 of #1427 phase 2 (schema v22): an optional, free-text author on the memory itself, usable without a room. The first open by a build with this change migrates the store in place (additiveALTER TABLE; a memory that belongs to exactly one room takes that room link's author, the rest stay unset).Memorygainsauthor(omitted from JSON when unset) andUteke::set_authorvalidates it (trimmed, 1 to 128 characters, no control characters). No HTTP, MCP or CLI input accepts it yet: that follows once uteke-cloud#219 is answered. - Internal: privacy filter detection module, part 1 of #1464 (
uteke_core::privacy):redactmasks AWS access keys, GitHub tokens,sk-API keys, bearer headers, JWTs, PEM private key blocks and long secret-named assignments (API_KEY=..., with an entropy check and exceptions for hashes, UUIDs, paths and placeholders), keeping the shape (ghp_[REDACTED:github-token]), with an allowlist for false positives. Nothing calls it yet: it is wired into the write paths, behind[privacy] mode, in part 2, after the cloud contract answer (uteke-cloud#218). - Internal: scope-isolation regression gate, parts 1 and 2 (#1467): the HTTP suite runs 20 scoped read requests through
route(every recall strategy, search, list, recent, export, graph, room, orphans, tags, stats, doc) and a test fails when a read route is added to the registry without an isolation check or a documented exemption. Part 1: a two-namespace fixture with overlapping vocabulary and a core suite asserting thatrecall(every strategy), unified recall,search,list, tags, export, room recall and graph views never return a record from another namespace. The fixture ships behind thetest-supportfeature ofuteke-corefor the server, MCP and CLI suites that follow. - Internal: scope-isolation regression gate, part 3 (#1467): the MCP suite over the read-only tools (a test fails when a tool is added without an isolation check or an exemption), a CLI suite that runs the binary against a two-namespace store, and
docs/isolation-suite.md, which lists what each transport covers and the exemptions. tools/listfor a read-only token lists only the tools it can call (#1446) - MCP over HTTP advertised every tool to read-only callers, which then failed at call time. A read-only caller now sees exactly the read tools; full-access callers, the stdio transport andtools/callare unchanged. Contract: uteke-cloud#203 (approved by the owner).unsafe_codeis now enforced (#1456) - the workspace declaredunsafe_code = "forbid"but no crate inherited it, so nothing was enforced. Every crate now has[lints] workspace = trueand the level isdeny: production code cannot containunsafe, and only the five test modules that callstd::env::set_var/remove_var(edition 2024) opt out with#[allow(unsafe_code)].forbidcannot be used because it cannot be relaxed per module.
📦 Downloads
Each archive contains three binaries + ONNX Runtime shared library:
| Binary | Description |
|---|---|
uteke |
CLI tool |
uteke-serve |
HTTP server daemon |
uteke-mcp |
MCP server (stdio + HTTP) |
libonnxruntime.so* / libonnxruntime*.dylib / onnxruntime*.dll |
ONNX Runtime |
| Platform | File |
|---|---|
| Linux x86_64 (AVX2) | uteke-x86_64-unknown-linux-gnu-v0.22.0.tar.gz |
| Linux x86_64 (Legacy, SSE4.2) | uteke-x86_64-unknown-linux-gnu-legacy-v0.22.0.tar.gz |
| Linux ARM64 | uteke-aarch64-unknown-linux-gnu-v0.22.0.tar.gz |
| macOS Apple Silicon | uteke-aarch64-apple-darwin-v0.22.0.tar.gz |
| Windows x86_64 | uteke-x86_64-pc-windows-msvc-v0.22.0.zip |
Legacy Bundle (Linux only) — Includes a SSE4.2-only ORT sidecar (
ort-legacy/) for CPUs without AVX2 (Intel Celeron J4125/N4020). Use this bundle to avoid SIGILL crashes on older hardware.
🚀 Quick Start
# Quick install (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/codecoradev/uteke/main/install.sh | sh
# Pin a specific version
UTEKE_VERSION=v0.22.0 curl -fsSL https://raw.githubusercontent.com/codecoradev/uteke/main/install.sh | sh
# Store a memory
uteke remember "Important context" --tags project
# Recall by meaning
uteke recall "what was that context?"
# Start server for fast AI agent access
uteke-serve --port 8767Full changelog: https://github.com/codecoradev/uteke/blob/main/CHANGELOG.md
Community contributors
(No external contributions in this cycle. Full contributor list: https://github.com/codecoradev/uteke/graphs/contributors)