Skip to content

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 30 Oct 03:48
· 394 commits to develop since this release
cad392d

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 openai or ollama backend 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.toml can no longer set the cycles that delete memories (#1534) - .uteke/uteke.toml in 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 or uteke aging run applied them to the user's store. These four sections are now read from the global uteke.toml only, 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 sets UTEKE_TRUST_PROJECT_CONFIG=1 to 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 import restores the vectors of imported documents, and uteke import --full (#1587) - a bundle from uteke export --full already 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. import now regenerates the chunk vectors after the restore and reports chunks_reembedded; uteke repair --reembed also re-embeds chunks without a vector (chunks_missing, chunks_reembedded, chunks_failed) when the backend was down during the import. --full states 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 export and uteke 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). import links every memory to the rooms that exist in the target store, also for a duplicate of a memory that is already stored, and reports room_links (new links) and rooms_missing (rooms named by the file that the target lacks) in the CLI, in POST /import and 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, or import.
  • 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 tool uteke_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 were remember with a room (a duplicate memory) or room 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 by benchmarks/scripts/score_longmemeval.py; scope of the metric), a README link, and benchmarks/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 the source is masked (or refused in strict mode) on remember, room remember, update, import, set_source and room consolidation, and counted in redactions. update_memory_detailed returns what an update masked; PUT /memory returns redacted and uteke_update reports 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 remember prints which kinds of secret were masked and its --json output gains redacted; uteke import reports masked and rejected rows; uteke_remember over MCP says so in its result, and in strict mode uteke_remember and uteke_update fail with a tool error that starts with secret_detected: and names the kinds, never the value.
  • [privacy] in uteke.toml (#1464, part 2b-2a) - mode (redact, the default, strict or off) and allowlist (regexes) now configure the secret filter in the CLI, uteke-serve and uteke-mcp; UTEKE_PRIVACY_MODE overrides 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.
  • redacted on the HTTP write responses and secret_detected in strict mode (#1464, part 2b-2b) - POST /remember and POST /room/remember return redacted: true when the privacy filter masked a secret; a strict-mode write is refused with 400 {"error": "secret_detected", "message", "kinds"} (also on PUT /memory); POST /import reports redacted and rejected_secret. Additive; agreed with cloud in uteke-cloud#218.
  • author on 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, MCP uteke_remember and uteke remember --author accept 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; the anonymous / user fallback of the room link is not an author. author is returned on the memory object by recall, search, list and get (omitted when unset), echoed by POST /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 import works 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)), unlike remember, 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, and uteke repair --reembed adds the vectors once a backend exists.

  • A namespace called all no 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 named all (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-mcp honours [embedding] and [vector] and the UTEKE_EMBEDDING_* variables (#1535, #1537 step 2) - the MCP binary opened the store with the built-in ONNX backend and default settings, so with backend = "openai" or "ollama" (or a custom dims) 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 other UTEKE_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 from develop and from the branch on the same configs.

  • The CLI no longer sends the ONNX model name to an openai or ollama endpoint (#1537, step 1) - with [embedding] backend = "openai" and no model, uteke sent model: "embeddinggemma-q4" to the endpoint (a real OpenAI rejects it) while uteke-serve on the same file sent the backend's default, text-embedding-3-small. The CLI's model now defaults to empty, like the server's, so an unset model is the backend's default everywhere (same with UTEKE_EMBEDDING_BACKEND set in the environment). Checked with binaries from develop and 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, dims or 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 for openai and ollama a 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_version 1); the first embeddings after the upgrade are recomputed. Embedder gains a provided method identity() (default name:dims), so existing implementations keep compiling.

  • UTEKE_HOME pointing at another store no longer routes to a running server (#1445) - with [server] enabled = true and uteke-serve running, 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 new GET /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 sends UTEKE_AUTH_TOKEN like 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.json gains /store in the lab list only.

Changed

  • uteke-serve embeds 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 an openai or ollama backend) queued every request behind it: throughput stayed flat however many clients there were. The router now embeds the query of POST /recall and the content of POST /remember first 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 with benchmarks/internal/scripts/serve_load.py against 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_remember gains source, source_type and author_type (#1516) - Uteke::run_remember runs the whole write: the strict-privacy check of the source, 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-contradiction write without a type now infers it like every other path (it stored fact). POST /room/remember is a separate operation and keeps its own request.
  • uteke remember --type is honoured on a local store and no longer defaults to fact (#1439, PR E2) - on a local store the flag was ignored unless --room or --detect-contradiction was set (the memory got the inferred type), and via a server the default fact was sent explicitly, so inference never ran. Now an explicit --type is stored (and copied into the metadata, as on POST /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, RememberPolicy and ForgetPolicy types are gone (#1439, PR G) - with every surface following the HTTP rules the policies had one value each. RecallRequest::decode, ListRequest::decode, RememberRequest::decode and ForgetRequest::decode take only the input, ForgetRequest::resolve and resolve_memory_id no longer take a policy, and IdCharset, FullIdForm and UnmatchedPrefixText are removed. No behaviour change; a library user of uteke-core drops the policy argument.
  • MCP uteke_forget and every MCP id argument follow the HTTP rules; uteke forget is the same on a local store and via a server (#1439, PR F) - an unknown prefix is Memory 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 covers uteke_get/update/pin/supersede, which share the resolver). A full UUID is normalised to hyphenated lowercase on every surface, so POST /forget?id= now accepts an upper-case or dash-less spelling of a stored id (it was a 404). uteke_forget refuses 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 forget now asks for the same confirmation as locally (the y/N prompt, or --confirm; a tag forget without --confirm prints the notice and deletes nothing), and --cold/--all are an explicit error instead of the misleading "Provide an ID" message.
  • MCP uteke_remember follows 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 omitted type is inferred from the content (it used to be stored as fact without inference), an unknown type is refused and a given type is copied into the memory's metadata as on POST /remember. A tags that is not an array of strings, a type that is not a string and any argument the tool does not declare are now errors (they were silently dropped). room stays MCP-only. In the CLI, --meta is parsed once for both a local store and a server (numbers, booleans, rel:type:target and a bare key were only understood locally); uteke remember --timestamp now works via a server (the anchor is applied before the request) and --room via a server is an explicit error instead of an "unknown field" rejection from the server.
  • MCP uteke_list follows the HTTP rules, and uteke list no 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 into Uteke::run_list (uteke_core::list_exec), used by POST /list and the MCP tool (HTTP behaviour unchanged). Over MCP the tool now accepts at (RFC3339, lists the memories that existed then) and include_meta (appends -- total N, has_more B, next_offset N to 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 --entity or --category with a running server used to be sent without the filter (the /list endpoint has none) and returned unfiltered rows; it now falls back to the local store, which applies them.
  • The default limit is 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 and uteke 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 MCP limit above 1000 is capped. Scripts that relied on the old defaults must pass --limit or limit; the LongMemEval harness passes --limit 50 and is unaffected. The MCP tool schemas and docs/cli-reference.md state 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), --limit above 100 is capped at 100 with a warning on stderr (there was no cap locally; the server and MCP already capped), and --at must 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 50 as the LongMemEval harness uses it behaves exactly as before.
  • MCP uteke_recall honours 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 into Uteke::run_recall (uteke_core::recall_exec), and both POST /recall and the MCP tool call it. HTTP behaviour is unchanged. Over MCP the tool now accepts entity, category, strict, enrich, at, after and before (it ignored them before) with the same guards as HTTP (pack and explain cannot be combined with the time filters, explain not with entity/category/enrich), and an argument the tool does not declare is an error that lists the supported ones (a typo such as limt used to be ignored silently); the list is built from the tool's own schema. The MCP default stays memories and documents.
  • MCP uteke_recall follows the HTTP rules (#1439, PR C part 1) - the same arguments now behave the same on POST /recall and over MCP: limit is capped at 100 (MCP had no cap), a blank or whitespace-only query is an error (MCP accepted it), tags: [] means no filter (MCP filtered on nothing, so it returned no results), an invalid limit (a string, a negative or a fractional number) is an error instead of silently becoming 5, and explain with any type is refused as on HTTP (explain is memory-only: omit type; MCP used to allow type: "memory"). The tool description says so. Agreed with cloud in uteke-cloud#173; cloud already clamps limit to 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 on uteke-serve, p50/p95/p99 per operation) and modal_perf.py (builds an exact commit on Modal, runs uteke bench and the load at several client counts, and writes a result file with a manifest). hot_path_bench (ignored tests in uteke-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, for aa75441, is in benchmarks/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, in docs/plans/. Documentation only.
  • Internal: rusqlite 0.41.0 (with libsqlite3-sys 0.39.1, bundled SQLite), replacing the dependabot PR #1551. Statement::column_names now returns an iterator, so the two column-existence checks in memory/schema.rs no longer call .iter(); no behaviour change.
  • Internal: operations.rs is split by concern, #1519 part 1 (round 2 of the architecture deepening, #1523). It is now the module directory operations/; 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 did memory_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_namespace and 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.rs keeps 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 /memory and uteke_update with a content equal 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 and created_at and never went through the dedup check of remember; tests now cover both.
  • room_delete says what it did (#1531) - the result on the MCP tool, the CLI and the HTTP note now 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-text note changed.
  • 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 dims the store was built with, or rebuild with UTEKE_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=1 still 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.rs has 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 by factory::build; Uteke::ensure_embedder shrank 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::EmbeddingSection is the section type both read, and EmbeddingSection::resolve is the only place that applies UTEKE_EMBEDDING_BACKEND (trimmed, blank = unset), validates the backend and applies the other UTEKE_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 set UTEKE_EMBEDDING_BACKEND or [embedding] backend) in the CLI too; a blank backend in 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-numeric UTEKE_EMBEDDING_DIMS warns from the core for every consumer. [embedding] max_seq_length and UTEKE_MAX_SEQ_LENGTH have 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 remember does (#1514, part 3) - a record written by room consolidation now goes through the same tail as remember: source hash, a created timeline 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 for remember) instead of rolling the row back; a failed room link still rolls it back. The created event is visible in GET /timeline and 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 remember does (#1514, part 2) - an imported row now goes through the same tail as remember: source hash, a created timeline 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 no created event (visible in GET /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 for remember; uteke repair backfills 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; the created event 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): when ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID and ANTHROPIC_WORKSPACE_ID are set it exchanges its GitHub OIDC token for a short-lived token (sent only to api.anthropic.com, model ANTHROPIC_MODEL, default claude-opus-5-5); otherwise it falls back to CORA_API_KEY, CORA_BASE_URL and CORA_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) and POST /consolidate/pair (missing id) answered 400 with a Validation 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 carries Internal 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 raises Error::NotFound at these five sites, so MCP and CLI messages lose the Validation error: prefix too. Agreed with cloud in uteke-cloud#244 (cloud follows for room/rename and consolidate/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 five expect("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; reembed takes 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_support gains open_with_embedder, open_with_embedder_at and a RecordingEmbedder (records every text, can fail its first calls); the 33 core tests that were #[ignore]d because Uteke::open needs 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-mcp used to open the store with the built-in defaults and ignore uteke.toml for recall, tier, graph rerank, dream, lifecycle and the embedding fallback. It now loads EngineSettings (global file, project file with its untrusted keys stripped, UTEKE_RECALL_* / UTEKE_GRAPH_* / UTEKE_EMBED_FALLBACK_*) and applies it, and uteke_recall uses [recall] min_score, min_score_strict and default_strategy when a call gives none, as POST /recall does; a call's own floor or strategy still wins. MCP over HTTP (POST /mcp) gets the same from the server's settings. uteke-serve now also honours the UTEKE_RECALL_* and UTEKE_GRAPH_* variables (it ignored them before). With no [recall] section and no variable nothing changes.
  • Internal: one EngineSettings and one apply step, #1515 part 3 (round 2 of the architecture deepening, #1523). uteke_core::EngineSettings holds [tier], [recall], [dream], [lifecycle] and [embed_fallback], and FallbackSettings is deserializable like the others; the CLI config and the server config embed it with #[serde(flatten)] instead of writing the five sections twice, and Uteke::apply_settings replaces 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 from develop and 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::RecallSettings owns the nine keys, their defaults and the UTEKE_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 drops RecallFileSection and its Option chains. The server still does not read the UTEKE_RECALL_* variables: that is decided in the apply step of part 3. No behaviour change, checked by running the CLI and the server built from develop and 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 implement serde::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 three Option mirror 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() and Uteke::graph_store() are no longer public (a breaking change for crates that depend on uteke-core); Uteke offers count_active, list_deprecated and graph() (the knowledge graph) instead, and store_for_tests() exists behind the test-support feature for fixtures. The private module of the Uteke room operations is now room_ops, so it no longer shares a name with memory::rooms. No behaviour change.
  • remember with detect_contradiction writes like plain remember (#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 like remember, 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::resolve decides which redacted / redactions flags a memory carries after an update as one pure function with a test per rule, and update_memory_detailed loses its inline five-arm match. The three flag helpers moved with it and are still reachable as uteke_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::NotFound and Error::class() (BadRequest, NotFound, SecretDetected, Internal), and one core_error mapping in the server instead of seven hand-written Validation => 400, else 500 arms. The two handlers that matched contains("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 the POST /doc/move 404, which no longer starts with Validation error:.
  • Internal: ort 2.0.0-rc.12/rc.13 to rc.14 (replaces the dependabot PR, which cannot build): rc.14 builds sessions from an explicit Environment, so the one process-wide environment is now created in embed::ort_init and handed to Session::builder. The bundled ONNX Runtime stays at 1.24.4 (api-18). Checked with the real model: remember writes the embedding and recall finds it.
  • Internal: privacy filter modes and flag helpers, part 2b-1a of #1464 (uteke_core::privacy): PrivacyMode (redact, strict, off), PrivacyConfig::prepare, Error::SecretDetected and the metadata.redacted/redactions helpers. 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 and Token/Bot auth, 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_memory and import mask 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_config takes PrivacyMode (Redact, the default, Strict or Off) and the allowlist; a masked memory gets metadata.redacted = true and metadata.redactions ([{kind, count}], never a value), a redacted timeline event, and RememberOutcome.redactions. The flags follow the content: an update to clean content drops them, a masked round trip or a metadata-only update keeps them. Strict returns the new Error::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 bare key, pass, token or secret is 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, 0x addresses, 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 and Token authorization 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 and source are not scanned, the structural import (uteke import of 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 named password holding a hyphenated phrase is masked, and nothing reads [privacy] from uteke.toml, so until part 2b-2 the default Redact applies 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 _PROD are 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.yaml because its secret-shaped assignments are the point of the test). uteke-core pins 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 (additive ALTER TABLE; a memory that belongs to exactly one room takes that room link's author, the rest stay unset). Memory gains author (omitted from JSON when unset) and Uteke::set_author validates 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): redact masks 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 that recall (every strategy), unified recall, search, list, tags, export, room recall and graph views never return a record from another namespace. The fixture ships behind the test-support feature of uteke-core for 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/list for 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 and tools/call are unchanged. Contract: uteke-cloud#203 (approved by the owner).
  • unsafe_code is now enforced (#1456) - the workspace declared unsafe_code = "forbid" but no crate inherited it, so nothing was enforced. Every crate now has [lints] workspace = true and the level is deny: production code cannot contain unsafe, and only the five test modules that call std::env::set_var/remove_var (edition 2024) opt out with #[allow(unsafe_code)]. forbid cannot 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 8767

Full 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)