From 2e9b62e2e8ed22d78eea73d4f24b70b16aa26efc Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Fri, 9 Oct 2026 11:36:08 +0700 Subject: [PATCH 1/3] fix(docker): skip the ONNX download for non-onnx backends and fail clearly on unwritable /data (#1398) (#1400) The entrypoint now resolves the embedding backend (UTEKE_EMBEDDING_BACKEND, then [embedding] backend in $UTEKE_HOME/uteke.toml, then onnx) and only downloads the local model for onnx. If a download is needed but the data dir is not writable it exits with an actionable message. Data dir follows UTEKE_HOME (default /data). Covered by integration tests that run the script with a fake curl and a fake uteke-serve. Co-authored-by: ajianaz Co-authored-by: Claude Sonnet 5.5 --- .../uteke-server/tests/docker_entrypoint.rs | 189 ++++++++++++++++++ docker-entrypoint.sh | 44 +++- 2 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 crates/uteke-server/tests/docker_entrypoint.rs diff --git a/crates/uteke-server/tests/docker_entrypoint.rs b/crates/uteke-server/tests/docker_entrypoint.rs new file mode 100644 index 00000000..8528acff --- /dev/null +++ b/crates/uteke-server/tests/docker_entrypoint.rs @@ -0,0 +1,189 @@ +//! `docker-entrypoint.sh` embedding-backend handling (#1398). +//! +//! The script is run for real with a fake `curl` (records that a download was +//! attempted) and a fake `uteke-serve` (proves the script reached `exec`). + +#![cfg(unix)] + +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +struct Sandbox { + root: PathBuf, +} + +impl Sandbox { + fn new() -> Self { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let root = std::env::temp_dir().join(format!( + "uteke_entrypoint_{}_{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::SeqCst) + )); + fs::create_dir_all(root.join("bin")).unwrap(); + fs::create_dir_all(root.join("data")).unwrap(); + // Fake curl: record the attempt and fail like an offline host. + write_exec( + &root.join("bin/curl"), + &format!( + "#!/bin/sh\necho \"$@\" >> {}/curl.log\nexit 22\n", + root.display() + ), + ); + // Fake server: proves the script reached `exec uteke-serve`. + write_exec( + &root.join("bin/uteke-serve"), + &format!( + "#!/bin/sh\necho \"started $@\" > {}/served.log\n", + root.display() + ), + ); + Self { root } + } + + fn data(&self) -> PathBuf { + self.root.join("data") + } + + fn run(&self, env: &[(&str, &str)]) -> Output { + let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docker-entrypoint.sh"); + let mut cmd = Command::new("sh"); + cmd.arg(script) + .arg("--port") + .arg("1") + .env_clear() + .env( + "PATH", + format!("{}:/usr/bin:/bin", self.root.join("bin").display()), + ) + .env("UTEKE_HOME", self.data()); + for (k, v) in env { + cmd.env(k, v); + } + cmd.output().expect("spawn entrypoint") + } + + fn downloaded(&self) -> bool { + self.root.join("curl.log").exists() + } + + fn served(&self) -> bool { + self.root.join("served.log").exists() + } +} + +impl Drop for Sandbox { + fn drop(&mut self) { + let _ = fs::set_permissions(self.data(), fs::Permissions::from_mode(0o755)); + let _ = fs::remove_dir_all(&self.root); + } +} + +fn write_exec(path: &Path, body: &str) { + fs::write(path, body).unwrap(); + fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap(); +} + +fn text(o: &Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr) + ) +} + +#[test] +fn external_backend_from_env_skips_the_download_and_starts_the_server() { + for backend in ["openai", "ollama"] { + let sb = Sandbox::new(); + let out = sb.run(&[("UTEKE_EMBEDDING_BACKEND", backend)]); + assert!(out.status.success(), "{}", text(&out)); + assert!(!sb.downloaded(), "{backend}: must not download"); + assert!(sb.served(), "{backend}: must reach exec"); + assert!(text(&out).contains("skipping"), "{}", text(&out)); + assert!(!sb.data().join("models").exists(), "no model dir created"); + } +} + +#[test] +fn external_backend_from_uteke_toml_skips_the_download() { + let sb = Sandbox::new(); + fs::write( + sb.data().join("uteke.toml"), + "[server]\nbackend = \"onnx\"\n\n[embedding]\n# comment\nbackend = \"openai\" # external\nmodel = \"m\"\n", + ) + .unwrap(); + let out = sb.run(&[]); + assert!(out.status.success(), "{}", text(&out)); + assert!(!sb.downloaded()); + assert!(sb.served()); +} + +#[test] +fn env_overrides_the_toml_backend() { + let sb = Sandbox::new(); + fs::write( + sb.data().join("uteke.toml"), + "[embedding]\nbackend = \"openai\"\n", + ) + .unwrap(); + // env says onnx, the model is missing and curl is offline: it must try to + // download (and fail), proving the env value won over the file. + let out = sb.run(&[("UTEKE_EMBEDDING_BACKEND", "onnx")]); + assert!(sb.downloaded(), "{}", text(&out)); + assert!(!out.status.success()); +} + +#[test] +fn only_the_embedding_section_decides() { + let sb = Sandbox::new(); + // `backend` under another section must not be mistaken for the embedder. + fs::write( + sb.data().join("uteke.toml"), + "[vector]\nbackend = \"vecq\"\n", + ) + .unwrap(); + let out = sb.run(&[]); + assert!(sb.downloaded(), "onnx default expected: {}", text(&out)); +} + +#[test] +fn onnx_with_the_model_present_does_not_download() { + let sb = Sandbox::new(); + let onnx = sb.data().join("models/embeddinggemma-q4/onnx"); + fs::create_dir_all(&onnx).unwrap(); + fs::write(onnx.join("model_q4.onnx"), b"x").unwrap(); + let out = sb.run(&[]); + assert!(out.status.success(), "{}", text(&out)); + assert!(!sb.downloaded()); + assert!(sb.served()); +} + +#[test] +fn unwritable_data_dir_fails_with_a_clear_message_instead_of_looping_silently() { + let sb = Sandbox::new(); + fs::set_permissions(sb.data(), fs::Permissions::from_mode(0o555)).unwrap(); + // root ignores directory permissions; nothing to assert there. + let writable_anyway = fs::create_dir(sb.data().join("probe")).is_ok(); + if writable_anyway { + return; + } + let out = sb.run(&[]); + assert!(!out.status.success()); + assert!(!sb.served()); + let msg = text(&out); + assert!(msg.contains("not writable"), "{msg}"); + assert!(msg.contains("UTEKE_EMBEDDING_BACKEND"), "{msg}"); +} + +#[test] +fn unwritable_data_dir_is_fine_for_an_external_backend() { + let sb = Sandbox::new(); + fs::set_permissions(sb.data(), fs::Permissions::from_mode(0o555)).unwrap(); + let out = sb.run(&[("UTEKE_EMBEDDING_BACKEND", "openai")]); + assert!(out.status.success(), "{}", text(&out)); + assert!(sb.served()); +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ed489c3d..c0e3a69e 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,7 +1,8 @@ #!/bin/sh set -e -MODEL_DIR="/data/models/embeddinggemma-q4" +DATA_DIR="${UTEKE_HOME:-/data}" +MODEL_DIR="${DATA_DIR}/models/embeddinggemma-q4" MODEL_FILE="${MODEL_DIR}/onnx/model_q4.onnx" # Expected SHA256 checksums for model files @@ -32,10 +33,45 @@ verify_sha256() { fi } -# Lazy download: only if model not present in volume -if [ ! -f "$MODEL_FILE" ]; then +# Embedding backend (#1398): UTEKE_EMBEDDING_BACKEND wins, then `[embedding] +# backend` in $UTEKE_HOME/uteke.toml, then the default (onnx). Only the onnx +# backend needs the local model; openai/ollama talk to an external embedder. +embedding_backend() { + if [ -n "${UTEKE_EMBEDDING_BACKEND:-}" ]; then + echo "$UTEKE_EMBEDDING_BACKEND" + return + fi + toml="${DATA_DIR}/uteke.toml" + if [ -f "$toml" ]; then + awk ' + /^[[:space:]]*\[/ { in_embedding = ($0 ~ /^[[:space:]]*\[embedding\][[:space:]]*(#.*)?$/) ; next } + in_embedding && /^[[:space:]]*backend[[:space:]]*=/ { + v = $0 + sub(/^[^=]*=[[:space:]]*/, "", v) + sub(/[[:space:]]*#.*$/, "", v) + gsub(/["\047[:space:]]/, "", v) + print v + exit + } + ' "$toml" + fi +} + +BACKEND="$(embedding_backend)" +BACKEND="${BACKEND:-onnx}" + +# Lazy download: only for the onnx backend, and only if the model is not +# present in the volume. +if [ "$BACKEND" != "onnx" ]; then + echo "Embedding backend is '${BACKEND}': skipping the local ONNX model download." +elif [ ! -f "$MODEL_FILE" ]; then echo "Model not found, downloading embedding model (~208MB)..." - mkdir -p "${MODEL_DIR}/onnx" + if ! mkdir -p "${MODEL_DIR}/onnx" 2>/dev/null; then + echo "ERROR: cannot create ${MODEL_DIR}: ${DATA_DIR} is not writable by $(id -un) (uid $(id -u))." >&2 + echo "Fix the volume permissions (chown the host directory to that uid), pre-populate the model there," >&2 + echo "or use an external embedder (UTEKE_EMBEDDING_BACKEND=openai|ollama) so no download is needed." >&2 + exit 1 + fi curl -fSL --retry 3 -o "${MODEL_DIR}/onnx/model_q4.onnx" \ "https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX/resolve/main/onnx/model_q4.onnx" From f7d1e59ef4ab978ee385105736a465499201a7ec Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Fri, 9 Oct 2026 11:36:14 +0700 Subject: [PATCH 2/3] fix(server): honour [embedding]/[vector] config and UTEKE_EMBEDDING_BACKEND (#1399) (#1401) uteke-serve opened the store with Uteke::open(), which hardcodes the onnx backend, so an external OpenAI-compatible or Ollama embedder could never be used. The server now resolves the embedder (env backend > [embedding] backend > onnx) and the [vector] engine and opens the store with open_with_embedding_and_graph, as the CLI does. An unsupported backend exits with an actionable error. The project-local file stays untrusted for embedding.* keys via the shared layered resolver. Co-authored-by: ajianaz Co-authored-by: Claude Sonnet 5.5 --- crates/uteke-server/src/main.rs | 181 +++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/crates/uteke-server/src/main.rs b/crates/uteke-server/src/main.rs index b1adeef5..8ec9b1f3 100644 --- a/crates/uteke-server/src/main.rs +++ b/crates/uteke-server/src/main.rs @@ -258,7 +258,29 @@ fn main() { info!("Opening store at: {db_path}"); let defaults = uteke_core::DreamConfig::default(); - let uteke = match Uteke::open(&db_path) { + // Embedding backend + vector engine from uteke.toml / env (#1399). Fail + // fast on an unsupported backend instead of opening the store with a + // different embedder than the operator asked for. + let embedding = match resolve_embedding( + &config, + std::env::var("UTEKE_EMBEDDING_BACKEND").ok().as_deref(), + ) { + Ok(e) => e, + Err(msg) => { + error!("{msg}"); + std::process::exit(1); + } + }; + info!("Embedding backend: {}", embedding.backend); + let uteke = match Uteke::open_with_embedding_and_graph( + &db_path, + &embedding.backend, + embedding.settings, + uteke_core::TierConfig::default(), + uteke_core::RecallConfig::default(), + uteke_core::GraphRerankConfig::default(), + embedding.vector_backend.as_deref(), + ) { Ok(mut u) => { // Apply dream pipeline thresholds from config (#731) if let Some(ref dc) = config.dream { @@ -664,6 +686,10 @@ fn main() { /// Minimal [server] config section for parsing uteke.toml. #[derive(serde::Deserialize, Default)] struct ServerFileConfig { + /// `[embedding]`: which embedder the server uses (#1399). + embedding: Option, + /// `[vector]`: which vector engine runs when both are compiled in (#1168). + vector: Option, server: Option, recall: Option, extraction: Option, @@ -675,6 +701,70 @@ struct ServerFileConfig { lifecycle: Option, } +/// `[embedding]` in uteke.toml, same keys as the CLI. Empty / missing values +/// fall back to the backend defaults; `UTEKE_EMBEDDING_*` env vars win over +/// these at resolve time inside the core. +#[derive(serde::Deserialize, Default, Clone)] +struct EmbeddingFileSection { + backend: Option, + model: Option, + api_key: Option, + base_url: Option, + endpoint_path: Option, + dims: Option, +} + +/// `[vector]` in uteke.toml. +#[derive(serde::Deserialize, Default, Clone)] +struct VectorFileSection { + backend: Option, +} + +/// Backends `Uteke` can initialize lazily (same list as the CLI validates). +const SUPPORTED_EMBEDDING_BACKENDS: &[&str] = &["onnx", "openai", "ollama"]; + +/// What the server opens the store with. +struct EmbeddingResolution { + backend: String, + settings: uteke_core::EmbeddingSettings, + vector_backend: Option, +} + +/// Resolve the embedder: `UTEKE_EMBEDDING_BACKEND` (non-empty) wins over +/// `[embedding] backend`, which wins over the `onnx` default. An unsupported +/// backend is an error with an actionable message (#1399). +fn resolve_embedding( + config: &ServerFileConfig, + env_backend: Option<&str>, +) -> Result { + let file = config.embedding.clone().unwrap_or_default(); + let backend = env_backend + .map(str::trim) + .filter(|b| !b.is_empty()) + .map(str::to_string) + .or_else(|| file.backend.clone().filter(|b| !b.trim().is_empty())) + .unwrap_or_else(|| "onnx".to_string()); + if !SUPPORTED_EMBEDDING_BACKENDS.contains(&backend.as_str()) { + return Err(format!( + "Unsupported embedding backend '{backend}'. Supported: {}. \ + Set UTEKE_EMBEDDING_BACKEND or [embedding] backend in uteke.toml.", + SUPPORTED_EMBEDDING_BACKENDS.join(", ") + )); + } + Ok(EmbeddingResolution { + backend, + settings: uteke_core::EmbeddingSettings { + api_key: file.api_key.unwrap_or_default(), + base_url: file.base_url.unwrap_or_default(), + endpoint_path: file.endpoint_path.unwrap_or_default(), + model: file.model.unwrap_or_default(), + dims: file.dims.unwrap_or(0), + }, + // UTEKE_VECTOR_BACKEND (higher precedence) is read inside the core. + vector_backend: config.vector.as_ref().and_then(|v| v.backend.clone()), + }) +} + /// Deprecated: superseded by [lifecycle] section (#934). Kept for backward-compat deserialization. #[derive(serde::Deserialize, Default, Clone)] #[allow(dead_code)] @@ -860,3 +950,92 @@ mod config_overlay_tests { assert_eq!(recall.default_strategy.as_deref(), Some("vector")); } } + +#[cfg(test)] +mod embedding_config_tests { + //! #1399 — the server honours `[embedding]` / `[vector]` / env. + use super::*; + + fn parse(toml_text: &str) -> ServerFileConfig { + toml::from_str(toml_text).expect("valid toml") + } + + #[test] + fn default_is_onnx_with_empty_settings() { + let r = resolve_embedding(&ServerFileConfig::default(), None).unwrap(); + assert_eq!(r.backend, "onnx"); + assert!(r.settings.base_url.is_empty() && r.settings.model.is_empty()); + assert_eq!(r.settings.dims, 0); + assert_eq!(r.vector_backend, None); + } + + #[test] + fn toml_selects_an_external_backend_and_passes_its_settings() { + let cfg = parse( + "[embedding]\nbackend = \"openai\"\nbase_url = \"http://embor:8355/v1\"\n\ + model = \"embeddinggemma-q4\"\ndims = 768\nendpoint_path = \"/embed\"\n\ + [vector]\nbackend = \"vecq\"\n", + ); + let r = resolve_embedding(&cfg, None).unwrap(); + assert_eq!(r.backend, "openai"); + assert_eq!(r.settings.base_url, "http://embor:8355/v1"); + assert_eq!(r.settings.model, "embeddinggemma-q4"); + assert_eq!(r.settings.endpoint_path, "/embed"); + assert_eq!(r.settings.dims, 768); + assert_eq!(r.vector_backend.as_deref(), Some("vecq")); + } + + #[test] + fn env_backend_wins_over_toml_and_empty_env_is_ignored() { + let cfg = parse("[embedding]\nbackend = \"ollama\"\n"); + assert_eq!( + resolve_embedding(&cfg, Some("openai")).unwrap().backend, + "openai" + ); + assert_eq!(resolve_embedding(&cfg, Some("")).unwrap().backend, "ollama"); + assert_eq!( + resolve_embedding(&cfg, Some(" ")).unwrap().backend, + "ollama" + ); + // env alone, no [embedding] section (the labs setup) + let r = resolve_embedding(&ServerFileConfig::default(), Some("openai")).unwrap(); + assert_eq!(r.backend, "openai"); + } + + #[test] + fn unsupported_backend_is_an_actionable_error() { + let err = resolve_embedding(&ServerFileConfig::default(), Some("cohere")) + .err() + .expect("must fail"); + assert!( + err.contains("cohere") && err.contains("onnx, openai, ollama"), + "{err}" + ); + assert!(err.contains("UTEKE_EMBEDDING_BACKEND"), "{err}"); + } + + #[test] + fn untrusted_project_file_cannot_pick_the_embedder() { + // The shared layered resolver strips embedding.backend / base_url from + // the project-local file unless the project config is trusted. + let dir = std::env::temp_dir().join(format!("uteke_emb_cfg_{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let global = dir.join("global.toml"); + let project = dir.join("project.toml"); + std::fs::write(&global, "[embedding]\nbackend = \"onnx\"\n").unwrap(); + std::fs::write( + &project, + "[embedding]\nbackend = \"openai\"\nbase_url = \"https://evil.example\"\n", + ) + .unwrap(); + let cfg = resolve_server_config(&uteke_core::config_layers::Layers { + global: Some(&global), + project: Some(&project), + trust_project: false, + }); + let r = resolve_embedding(&cfg, None).unwrap(); + assert_eq!(r.backend, "onnx"); + assert!(r.settings.base_url.is_empty()); + let _ = std::fs::remove_dir_all(&dir); + } +} From 172f07c0d851ad3ae58c1f76c6e2d43d3999131a Mon Sep 17 00:00:00 2001 From: ajianaz Date: Fri, 9 Oct 2026 14:04:33 +0700 Subject: [PATCH 3/3] chore(release): v0.20.1 - version bump, changelog, docs sync Patch release with the two embedder fixes (#1398, #1399) cherry-picked from develop; version files, skill version lines, core-contract.json and the CHANGELOG entry updated. Co-Authored-By: Claude Sonnet 5.5 --- .agents/skills/uteke-memory/SKILL.md | 2 +- CHANGELOG.md | 9 +++++++++ Cargo.lock | 8 ++++---- Cargo.toml | 2 +- README.id.md | 2 +- README.md | 2 +- crates/uteke-cli/Cargo.toml | 2 +- crates/uteke-cli/assets/uteke-memory-skill.md | 2 +- crates/uteke-mcp/Cargo.toml | 2 +- crates/uteke-server/Cargo.toml | 4 ++-- docs/core-contract.json | 2 +- 11 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.agents/skills/uteke-memory/SKILL.md b/.agents/skills/uteke-memory/SKILL.md index e652382b..11fe6e27 100644 --- a/.agents/skills/uteke-memory/SKILL.md +++ b/.agents/skills/uteke-memory/SKILL.md @@ -5,7 +5,7 @@ description: "Persistent memory engine for AI agents via the uteke CLI — remem # Uteke Memory Skill Persistent memory engine for AI agents via the `uteke` CLI. -Version: **0.20.0** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide. +Version: **0.20.1** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide. > This skill ships with each release — its version tracks the CLI version > (a CI gate fails when they drift, see `skill-version-parity` test). diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3a03a0..196c4a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.20.1] - 2026-10-09 + +Patch release. Theme: **use an external OpenAI-compatible or Ollama embedder with `uteke-serve` and the Docker image.** Two fixes, nothing else (no schema change, no API change). + +### Fixed + +- **`uteke-serve` ignored the embedding configuration (#1399, #1401)** - the server opened its store with a hardcoded `onnx` backend, `ServerFileConfig` had no `[embedding]` section and `UTEKE_EMBEDDING_BACKEND` was only read by the CLI, so an external OpenAI-compatible or Ollama embedder could never be used by the server. The server now reads `[embedding]` (backend, model, api_key, base_url, endpoint_path, dims) and `[vector]` from `uteke.toml`; `UTEKE_EMBEDDING_BACKEND` (non-empty) wins over the file, `onnx` stays the default, and an unsupported backend exits at startup with a message naming the supported ones. The project-local `.uteke/uteke.toml` stays untrusted for `embedding.*`. Deployments without these settings behave exactly as before. +- **Docker entrypoint downloaded the ONNX model for every backend and looped when `/data` was not writable (#1398, #1400)** - with `UTEKE_EMBEDDING_BACKEND=openai|ollama` (env, or `[embedding] backend` in `$UTEKE_HOME/uteke.toml`) the ~208 MB model download is now skipped. When a download is needed but the data dir is not writable, the container exits with a message naming the fixes (chown the volume to uid 1000, pre-populate the model, or use an external embedder) instead of dying on a bare `mkdir` error and restarting forever. The server still needs a writable data dir for `uteke.db` and the index files. + ## [0.20.0] - 2026-10-08 Minor release. Theme: **one write policy for the vector index, atomic documents, diff --git a/Cargo.lock b/Cargo.lock index aa97c954..f1d6b054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2767,7 +2767,7 @@ dependencies = [ [[package]] name = "uteke-cli" -version = "0.20.0" +version = "0.20.1" dependencies = [ "chrono", "clap", @@ -2793,7 +2793,7 @@ dependencies = [ [[package]] name = "uteke-core" -version = "0.20.0" +version = "0.20.1" dependencies = [ "chrono", "dirs", @@ -2818,7 +2818,7 @@ dependencies = [ [[package]] name = "uteke-mcp" -version = "0.20.0" +version = "0.20.1" dependencies = [ "chrono", "dirs", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "uteke-server" -version = "0.20.0" +version = "0.20.1" dependencies = [ "chrono", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index dd466da1..0fe5b446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.20.0" +version = "0.20.1" edition = "2024" license = "Apache-2.0" repository = "https://github.com/codecoradev/uteke" diff --git a/README.id.md b/README.id.md index 5c7ed757..8506dccb 100644 --- a/README.id.md +++ b/README.id.md @@ -392,7 +392,7 @@ Bisa. Uteke punya MCP server yang langsung pakai dengan Claude Code, Cursor, dan
Sudah production-ready? -Uteke sekarang v0.20.0 dengan 200+ test, CI/CD di setiap commit, dan benchmark harness. Dipakai production oleh tim CodeCora dan early adopter lain. Masih di versi 0.x, jadi mungkin ada rough edges, tapi core-nya udah stabil. +Uteke sekarang v0.20.1 dengan 200+ test, CI/CD di setiap commit, dan benchmark harness. Dipakai production oleh tim CodeCora dan early adopter lain. Masih di versi 0.x, jadi mungkin ada rough edges, tapi core-nya udah stabil.
--- diff --git a/README.md b/README.md index f770a7ba..103fc44b 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ Yes. Uteke ships with an MCP server that works with Claude Code, Cursor, and Her
Is it production-ready? -Uteke is at v0.20.0 with 200+ tests, CI/CD on every commit, and a benchmark harness. It's used in production by the CodeCora team and other early adopters. Still in 0.x, so expect rough edges, but the core is stable. +Uteke is at v0.20.1 with 200+ tests, CI/CD on every commit, and a benchmark harness. It's used in production by the CodeCora team and other early adopters. Still in 0.x, so expect rough edges, but the core is stable.
--- diff --git a/crates/uteke-cli/Cargo.toml b/crates/uteke-cli/Cargo.toml index d409f2cd..273c7c85 100644 --- a/crates/uteke-cli/Cargo.toml +++ b/crates/uteke-cli/Cargo.toml @@ -22,7 +22,7 @@ usearch = ["uteke-core/usearch"] vecq = ["uteke-core/vecq"] [dependencies] -uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] } +uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] } clap = { version = "4", features = ["derive"] } serde_json = "1" tracing = "0.1" diff --git a/crates/uteke-cli/assets/uteke-memory-skill.md b/crates/uteke-cli/assets/uteke-memory-skill.md index e652382b..11fe6e27 100644 --- a/crates/uteke-cli/assets/uteke-memory-skill.md +++ b/crates/uteke-cli/assets/uteke-memory-skill.md @@ -5,7 +5,7 @@ description: "Persistent memory engine for AI agents via the uteke CLI — remem # Uteke Memory Skill Persistent memory engine for AI agents via the `uteke` CLI. -Version: **0.20.0** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide. +Version: **0.20.1** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide. > This skill ships with each release — its version tracks the CLI version > (a CI gate fails when they drift, see `skill-version-parity` test). diff --git a/crates/uteke-mcp/Cargo.toml b/crates/uteke-mcp/Cargo.toml index 80beb2c0..b89cecbb 100644 --- a/crates/uteke-mcp/Cargo.toml +++ b/crates/uteke-mcp/Cargo.toml @@ -29,7 +29,7 @@ usearch = ["uteke-core/usearch"] vecq = ["uteke-core/vecq"] [dependencies] -uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] } +uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] } serde = { version = "1", features = ["derive"] } serde_json = "1" dirs = "7" diff --git a/crates/uteke-server/Cargo.toml b/crates/uteke-server/Cargo.toml index 8aa8450d..a95a023f 100644 --- a/crates/uteke-server/Cargo.toml +++ b/crates/uteke-server/Cargo.toml @@ -27,8 +27,8 @@ name = "uteke-serve" path = "src/main.rs" [dependencies] -uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] } -uteke-mcp = { path = "../uteke-mcp", version = "0.20.0", default-features = false } +uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] } +uteke-mcp = { path = "../uteke-mcp", version = "0.20.1", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" schemars = { version = "1", optional = true } diff --git a/docs/core-contract.json b/docs/core-contract.json index 58ee31bc..8dcf7b85 100644 --- a/docs/core-contract.json +++ b/docs/core-contract.json @@ -319,5 +319,5 @@ } ], "source_of_truth": "uteke PROD store, document tree kontrak-core-cloud", - "version": "0.20.0" + "version": "0.20.1" }