chore: post-release sync main→develop + AGENTS.md (0.17.0) - #1198
Merged
Conversation
…ory-fields feat: add full memory detail fields to UnifiedSearchResult
…ssing columns A) get_related() in graph.rs was doing load_all(None) to find reverse metadata relationships — O(n) full table scan. The edge_bfs() method already handles bidirectional lookups via UNION on source_id/target_id using indexed SQL. Removed the full-table scan. Forward-only metadata fallback kept for pre-v8 stores (O(degree), not O(n)). B) recall_room() SQL in memory/rooms.rs was missing m.slug, m.source, m.source_type columns (only selected up to content_type=col16). row_to_memory() expects 20 columns (0-19). Fixed all 4 SQL variants. Refs #689
fix: remove O(n) reverse scan in get_related() + fix recall_room() missing columns
New room_documents table with room_id + doc_slug composite PK. Enables explicit room-to-document linking beyond memory wikilinks. Store methods: - room_add_document(room_id, doc_slug) - room_remove_document(room_id, doc_slug) - room_list_documents(room_id) → Vec<slug> - document_list_rooms(doc_slug) → Vec<room_id> Server endpoints: - PUT /room/document/add - DELETE /room/document/remove - POST /room/document/list - POST /doc/room/list Refs #689
- Add room_add_document, room_remove_document, room_list_documents, document_list_rooms to Uteke public API (rooms.rs) - Replace uteke.store.xxx with uteke.xxx in server handlers - Add missing ns binding in /tags GET handler - Apply cargo fmt
When [[slug]] in memory content doesn't match any memory, Uteke now checks if it matches a document slug. If so, creates a 'references_doc' edge (auto-backlinked as 'referenced_by'). New API: - POST /memory/doc-refs → document slugs referenced by a memory - POST /doc/mem-refs → memory IDs referencing a document - Uteke::recall_memories_for_document(slug) - Uteke::recall_documents_for_memory(memory_id) - Store::edge_targets() / edge_sources() — generic typed edge queries UnifiedSearchResult enriched with: - linked_doc_slugs (for memory results) - linked_memory_ids (for document results) Refs #689
- Replace read_json() (nonexistent) with read_body<T>() pattern in /memory/doc-refs and /doc/mem-refs handlers - Add missing let ns = parse_query_namespace(&path) in /tags handler - Apply cargo fmt
…f-689 feat: memory↔document cross-entity linking via [[doc-slug]] wikilinks
The handlers used json!() which is not in scope — must be serde_json::json!() to match the rest of handlers.rs.
…on-689 feat: room↔document junction table (schema v15)
- Remove _bmad/ folder (BMAD config/templates, not uteke-related) - Remove .agents/skills/bmad-* (48 BMAD skill folders, ~14MB) - Remove .agents/skills/wds-* (14 WDS skill folders, ~5MB) - Remove .agents/skills/memory + sync (WDS session skills) - Keep .agents/skills/uteke-memory (legitimate uteke skill, bump 0.6.7→0.7.3) Docs updates (v0.7.3→HEAD): - architecture.md: schema v13→v15, new endpoint tables (Memory Mutation, Room↔Document Junction, Cross-Entity References), room_documents info - cli-reference.md: 9 new HTTP endpoints documented - mcp.md: tool count 27→29, add graph_add_edge + graph_remove_edge - CHANGELOG.md: populate [Unreleased] with all changes since v0.7.3 - README.md: add Partial Updates + Cross-Entity Linking features
The /proc/self/cmdline approach is unreliable because shell hooks run as child subprocesses whose cmdline is the handler script, not the gateway process. This caused agent name resolution to fail when hooks were invoked from asyncio contexts (e.g., session expiry watcher). Changes: - Replace /proc/self/cmdline with cwd-based resolution from payload - Dynamically resolve agent per invocation (not at module load time) - Remove hardcoded UTEKE_BIN path check (assume uteke on PATH) - Remove cron session skip (handler-level concern, not docs example) - Pass agent name to recall function explicitly
Cora review: docstring claimed 'Priority: cwd > HERMES_PROFILE env > fallback' but code only checks cwd then returns 'default'. Fix docstring to match actual behavior.
docs: fix Mode C handler example — cwd-based agent resolution
…date-docs chore: remove bmad/wds skills, update docs for v0.7.3→v0.7.4
Mode B (memory-provider) was removed for Hermes on 2026-06-29 but the docs still presented it as an active option alongside Mode A and C. Changes: - Architecture diagram: label Mode B as deprecated, add Mode C - Comparison table: strikethrough Mode B column, reorder A/C first - Mode B section: collapse 96 lines of deprecated content into 10-line blockquote with migration links - Mode comparison summary: same strikethrough treatment - Requirements: remove Mode B entry - Non-Hermes agents section: unchanged (already has Hermes warning) Result: 527 → 446 lines (-81 lines). Active modes (A+C) are now the primary focus; Mode B is preserved as historical reference only.
Guides new users from zero to productive in one command: - Detects install (binary on PATH, existing store) - Asks which AI agent they use (Hermes/Claude/Cursor/Pi/OpenCode) - Picks integration mode (tool vs memory-provider) - Toggles features on/off (Aging, Auto-maintenance, Graph rerank, Salience/Recency boost, Server mode) - Writes uteke.toml config with selections - Runs uteke init with selected agent automatically - Prints a full feature showcase (all commands grouped by category) Non-interactive mode: uteke onboard --yes --agent hermes --namespace default Docs updated: getting-started.md, cli-reference.md, AGENT.md, CHANGELOG.md New dep: which crate for PATH detection
Add a prominent "🚀 Onboarding" section right after install, before "Why Uteke?". Includes an agent-readable callout so when a user drops the GitHub link to an AI agent (Hermes, Claude, etc.), the agent reads the README and knows to run `uteke onboard.
Rename of the Mode B heading (-> '~~Mode B~~ — memory-provider (deprecated)') orphaned the cross-reference at line 245. Update the link target to the new anchor #mode-b--memory-provider-deprecated. Verified via github-slugger: 0 broken internal links.
The heading 'Memory-Provider for Other Agents (#575/#577)' produced a confusing concatenated anchor #memory-provider-for-other-agents-575577. Drop the issue refs from the heading (move into the body) so the anchor becomes the clean #memory-provider-for-other-agents. Fixes CodeCora alert. Verified via github-slugger: 0 broken links; no other file references the old anchor.
docs: de-emphasize deprecated Mode B in Hermes integration
…ortance
The /memory/importance handler classified validation errors via
e.to_string().contains("importance must be"), coupling the 400/500
status to the exact wording of the validation message in store.rs.
If that wording changes, a 400 would silently degrade to a 500.
Match the typed Error::Validation(_) variant instead, mirroring the
pattern already used by PUT /memory.
Closes #697
The room_documents junction table has an FK on room_id (→ rooms) but none on doc_slug, so POST /room/document/add accepted any doc_slug and created dangling room→document links. A bogus room_id also surfaced as a raw FK violation → 500. Validate both room_id and doc_slug existence in room_add_document, returning Error::Validation (→ 400) for unknown values. Update the PUT /room/document/add handler to map Validation → 400 accordingly. Closes #698
…patch fix(server): match Error::Validation instead of string in /memory/importance
…tion fix(core): validate room_id + doc_slug exist in room_add_document
🔴 Must Fix: - AGENT.md: correct schema version v13 → v15 (room↔document junction, #689) - onboard.rs: remove misleading get_stats() placeholder, drop count claim - onboard.rs: write_config() now backs up existing uteke.toml to .bak and prompts for confirmation before overwriting (prevents credential loss) 🟡 Should Fix: - prompt_integration_mode(): print message for custom agents (no longer silent) - validate_agent(): now warns on unrecognized agents instead of being a no-op - Banner boxes: replace misaligned Unicode box drawing with consistent ASCII print_banner() helper (fixes off-by-1 alignment, ambiguous em-dash width) - --json flag: intentionally ignored in onboard to avoid mixing ASCII banners with parseable JSON output 🟢 Nice to Have: - Install URL: main → develop (matches repo default branch convention) - Add 7 unit tests for pure functions (get_toggle, validate_agent, print_banner, write_config TOML structure) All tests pass: cargo fmt --check ✅, cargo clippy -D warnings ✅, cargo test (50 passed) ✅ Co-authored-by: ajianaz <reviewer>
docs: publish v0.12.0 documentation and codebase to main
chore: release v0.13.0 — Safe Memory Lifecycle
chore: merge develop to main — gcc-13 fix + changelog
chore: gcc-13 PPA fix for release build
chore: ubuntu-24.04 runner fix for Linux builds
chore: release notes for v0.13.0
fix(ci): cargo-zigbuild for GLIBC 2.17 broad compatibility
fix(ci): ziglang PATH fix for Linux builds
fix(ci): zig binary path resolution for cargo-zigbuild
fix(ci): resolve both zig and cargo-zigbuild binary paths
fix(ci): cargo-zigbuild --help check
fix(ci): rollback zigbuild, use ubuntu-22.04 runners
fix(ci): ubuntu-24.04 standard build for numkong GCC 14
chore: release v0.13.1 to main
merge: develop → main v0.13.2
* fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * ci: trigger security workflow on PRs to main --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: crates.io publish race condition + uteke-mcp metadata (#1021) - Add 60s/30s/30s delay between crate publishes for index propagation - Add repository, rust-version, documentation, homepage to uteke-mcp - Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index - Fixes: 'manifest has no documentation, homepage or repository' warning Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): switch deploy-website from npm to bun (#1022) Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking and added it to .gitignore. The deploy-website workflow still referenced it via cache-dependency-path, causing setup-node to fail: 'Some specified paths were not resolved, unable to cache dependencies.' Fix: - Replace actions/setup-node with oven-sh/setup-bun - Replace npm ci with bun install --frozen-lockfile - Replace npm run build with bun run build - Add docs/bun.lock as tracked lockfile - Remove docs/package-lock.json from .gitignore (no longer relevant) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat(test): adopt cargo-mutants for mutation testing (#1023) Add mutation testing infrastructure to validate test quality across critical pure-logic modules. 11 new mutation-killing tests improve salience_recency.rs score from 76% to 96%. Changes: - Add cargo-mutants config (mutants.toml) with exclusions for modules requiring external services - Add mutation-testing CI workflow (develop→main PRs only, pre-release quality gate) - Add .gitignore entries for mutants output directories - Add mutation-testing.md developer documentation - Write 9 mutation-killing tests for salience_recency.rs - Write 2 mutation-killing tests for recall_cache.rs Results (cargo-mutants v27.1.0): jaccard.rs: 12 mutants, 9 caught, 0 missed (100%) salience_recency: 53 mutants, 48 caught, 3 missed (96%) recall_cache: 25 mutants, 18 caught, 5 missed (80%) Refs: nginjen mutation testing pattern (#mutation-testing) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024) Two production bugs found by mutation testing: 1. Heading duplication: oversized markdown sections had their heading prepended twice in the first sub-chunk (once by split_by_headings, once more by the sub-chunk loop), corrupting downstream embeddings. Fixed by removing the dead heading_prefix re-prepend path entirely. 2. Multibyte infinite loop: split_long_text's zero-progress guard advanced by raw byte offsets that could land inside multi-byte UTF-8 characters (CJK/emoji), flooring back to start forever. chunk_markdown("日本語", 2) would hang. Fixed with a proper forward-char-boundary advance in the guard. Also: - 40 new mutation-killing tests (chunker: 20 → 60 tests) - cargo-mutants config moved to .cargo/mutants.toml (v27 schema: exclude_globs/exclude_re, valid keys only); glob paths fixed to match from workspace root; CLI update_check also excluded - 3 proven-equivalent mutants excluded with documented reasoning - docs/mutation-testing.md: final scores, bug postmortem, timeout notes Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.1 (#1025) Patch release: chunker heading duplication fix, multibyte infinite loop fix, mutation testing hardening (PR #1024). - Bump workspace version 0.14.0 -> 0.14.1 - Bump intra-workspace deps (cli/mcp/server -> core 0.14.1) - CHANGELOG entry for 0.14.1 Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: crates.io publish race condition + uteke-mcp metadata (#1021) - Add 60s/30s/30s delay between crate publishes for index propagation - Add repository, rust-version, documentation, homepage to uteke-mcp - Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index - Fixes: 'manifest has no documentation, homepage or repository' warning Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): switch deploy-website from npm to bun (#1022) Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking and added it to .gitignore. The deploy-website workflow still referenced it via cache-dependency-path, causing setup-node to fail: 'Some specified paths were not resolved, unable to cache dependencies.' Fix: - Replace actions/setup-node with oven-sh/setup-bun - Replace npm ci with bun install --frozen-lockfile - Replace npm run build with bun run build - Add docs/bun.lock as tracked lockfile - Remove docs/package-lock.json from .gitignore (no longer relevant) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat(test): adopt cargo-mutants for mutation testing (#1023) Add mutation testing infrastructure to validate test quality across critical pure-logic modules. 11 new mutation-killing tests improve salience_recency.rs score from 76% to 96%. Changes: - Add cargo-mutants config (mutants.toml) with exclusions for modules requiring external services - Add mutation-testing CI workflow (develop→main PRs only, pre-release quality gate) - Add .gitignore entries for mutants output directories - Add mutation-testing.md developer documentation - Write 9 mutation-killing tests for salience_recency.rs - Write 2 mutation-killing tests for recall_cache.rs Results (cargo-mutants v27.1.0): jaccard.rs: 12 mutants, 9 caught, 0 missed (100%) salience_recency: 53 mutants, 48 caught, 3 missed (96%) recall_cache: 25 mutants, 18 caught, 5 missed (80%) Refs: nginjen mutation testing pattern (#mutation-testing) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024) Two production bugs found by mutation testing: 1. Heading duplication: oversized markdown sections had their heading prepended twice in the first sub-chunk (once by split_by_headings, once more by the sub-chunk loop), corrupting downstream embeddings. Fixed by removing the dead heading_prefix re-prepend path entirely. 2. Multibyte infinite loop: split_long_text's zero-progress guard advanced by raw byte offsets that could land inside multi-byte UTF-8 characters (CJK/emoji), flooring back to start forever. chunk_markdown("日本語", 2) would hang. Fixed with a proper forward-char-boundary advance in the guard. Also: - 40 new mutation-killing tests (chunker: 20 → 60 tests) - cargo-mutants config moved to .cargo/mutants.toml (v27 schema: exclude_globs/exclude_re, valid keys only); glob paths fixed to match from workspace root; CLI update_check also excluded - 3 proven-equivalent mutants excluded with documented reasoning - docs/mutation-testing.md: final scores, bug postmortem, timeout notes Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.1 (#1025) Patch release: chunker heading duplication fix, multibyte infinite loop fix, mutation testing hardening (PR #1024). - Bump workspace version 0.14.0 -> 0.14.1 - Bump intra-workspace deps (cli/mcp/server -> core 0.14.1) - CHANGELOG entry for 0.14.1 Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(cli): uteke-cli crates.io publish failure + release verify step (#1030) * fix(cli): embed bundled assets inside crate root for crates.io publish include_str! paths pointed outside the package root (../../../.agents, ../../../extensions). cargo publish cannot bundle files outside the package, so uteke-cli has failed to publish since June (stuck at 0.4.3) while the release workflow hid the failure with continue-on-error. Assets now live in crates/uteke-cli/assets/ and ship inside the .crate. Verified locally: cargo package -p uteke-cli passes with full verify. * ci(release): verify crates.io versions after publish The continue-on-error on publish steps (added for the tag race fix) also hides genuine publish failures. Verify all four crates report the tagged version on crates.io; fail the job if any crate lags behind. * style: cargo fmt --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.2 (#1031) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: crates.io publish race condition + uteke-mcp metadata (#1021) - Add 60s/30s/30s delay between crate publishes for index propagation - Add repository, rust-version, documentation, homepage to uteke-mcp - Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index - Fixes: 'manifest has no documentation, homepage or repository' warning Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): switch deploy-website from npm to bun (#1022) Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking and added it to .gitignore. The deploy-website workflow still referenced it via cache-dependency-path, causing setup-node to fail: 'Some specified paths were not resolved, unable to cache dependencies.' Fix: - Replace actions/setup-node with oven-sh/setup-bun - Replace npm ci with bun install --frozen-lockfile - Replace npm run build with bun run build - Add docs/bun.lock as tracked lockfile - Remove docs/package-lock.json from .gitignore (no longer relevant) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat(test): adopt cargo-mutants for mutation testing (#1023) Add mutation testing infrastructure to validate test quality across critical pure-logic modules. 11 new mutation-killing tests improve salience_recency.rs score from 76% to 96%. Changes: - Add cargo-mutants config (mutants.toml) with exclusions for modules requiring external services - Add mutation-testing CI workflow (develop→main PRs only, pre-release quality gate) - Add .gitignore entries for mutants output directories - Add mutation-testing.md developer documentation - Write 9 mutation-killing tests for salience_recency.rs - Write 2 mutation-killing tests for recall_cache.rs Results (cargo-mutants v27.1.0): jaccard.rs: 12 mutants, 9 caught, 0 missed (100%) salience_recency: 53 mutants, 48 caught, 3 missed (96%) recall_cache: 25 mutants, 18 caught, 5 missed (80%) Refs: nginjen mutation testing pattern (#mutation-testing) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024) Two production bugs found by mutation testing: 1. Heading duplication: oversized markdown sections had their heading prepended twice in the first sub-chunk (once by split_by_headings, once more by the sub-chunk loop), corrupting downstream embeddings. Fixed by removing the dead heading_prefix re-prepend path entirely. 2. Multibyte infinite loop: split_long_text's zero-progress guard advanced by raw byte offsets that could land inside multi-byte UTF-8 characters (CJK/emoji), flooring back to start forever. chunk_markdown("日本語", 2) would hang. Fixed with a proper forward-char-boundary advance in the guard. Also: - 40 new mutation-killing tests (chunker: 20 → 60 tests) - cargo-mutants config moved to .cargo/mutants.toml (v27 schema: exclude_globs/exclude_re, valid keys only); glob paths fixed to match from workspace root; CLI update_check also excluded - 3 proven-equivalent mutants excluded with documented reasoning - docs/mutation-testing.md: final scores, bug postmortem, timeout notes Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.1 (#1025) Patch release: chunker heading duplication fix, multibyte infinite loop fix, mutation testing hardening (PR #1024). - Bump workspace version 0.14.0 -> 0.14.1 - Bump intra-workspace deps (cli/mcp/server -> core 0.14.1) - CHANGELOG entry for 0.14.1 Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(cli): uteke-cli crates.io publish failure + release verify step (#1030) * fix(cli): embed bundled assets inside crate root for crates.io publish include_str! paths pointed outside the package root (../../../.agents, ../../../extensions). cargo publish cannot bundle files outside the package, so uteke-cli has failed to publish since June (stuck at 0.4.3) while the release workflow hid the failure with continue-on-error. Assets now live in crates/uteke-cli/assets/ and ship inside the .crate. Verified locally: cargo package -p uteke-cli passes with full verify. * ci(release): verify crates.io versions after publish The continue-on-error on publish steps (added for the tag race fix) also hides genuine publish failures. Verify all four crates report the tagged version on crates.io; fail the job if any crate lags behind. * style: cargo fmt --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.2 (#1031) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * ci(release): reject stale RELEASE_NOTES.md (#1033) The v0.14.2 release reused the v0.13.0 notes because a committed RELEASE_NOTES.md is preferred over auto-generation and nothing checked which version it was for. The release shipped with wrong notes until manually fixed. The guard now requires the file to mention the tagged version. Also remove the stale v0.13.0 file so the next release auto-generates from the CHANGELOG unless someone writes fresh notes. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: HTTP & MCP recall strategy — default hybrid, loud validation (#1034, #1035) (#1038) * fix(server,mcp): default recall strategy to hybrid, validate strategy at boundary (#1034, #1035) HTTP: resolve strategy once (request > [recall] default_strategy config > hybrid). Invalid strategy returns 400 on all paths (bare recall, unified search, v1). The eager legacy recall that ran before strategy resolution is removed. Memory-only recall path routes through recall_hybrid with the same 3x over-fetch post-filter pattern used by recall_unified_memories (entity/category filters). MCP: uteke_recall schema exposes strategy (vector|fts5|hybrid|graph). Default resolves to hybrid, invalid values return a loud JSON-RPC error (-32603) instead of silently falling back to vector. Server startup: sanitize [recall] default_strategy from uteke.toml — invalid value warns and falls back to hybrid so a config typo cannot 400 every request with a message blaming the request. Docs: api-reference.md and mcp.md now describe the hybrid default, HTTP 400 on invalid, and the config fallback chain. Verified empirically against a scratch store: HTTP matrix 10/10, MCP harness 8/8 including engine parity (default == hybrid, warm cache) and loud bogus rejection. * docs: regenerate api-reference via docgen (strategy hybrid default, 400 on invalid) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.3 (#1039) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…1042) * fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: crates.io publish race condition + uteke-mcp metadata (#1021) - Add 60s/30s/30s delay between crate publishes for index propagation - Add repository, rust-version, documentation, homepage to uteke-mcp - Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index - Fixes: 'manifest has no documentation, homepage or repository' warning Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): switch deploy-website from npm to bun (#1022) Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking and added it to .gitignore. The deploy-website workflow still referenced it via cache-dependency-path, causing setup-node to fail: 'Some specified paths were not resolved, unable to cache dependencies.' Fix: - Replace actions/setup-node with oven-sh/setup-bun - Replace npm ci with bun install --frozen-lockfile - Replace npm run build with bun run build - Add docs/bun.lock as tracked lockfile - Remove docs/package-lock.json from .gitignore (no longer relevant) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat(test): adopt cargo-mutants for mutation testing (#1023) Add mutation testing infrastructure to validate test quality across critical pure-logic modules. 11 new mutation-killing tests improve salience_recency.rs score from 76% to 96%. Changes: - Add cargo-mutants config (mutants.toml) with exclusions for modules requiring external services - Add mutation-testing CI workflow (develop→main PRs only, pre-release quality gate) - Add .gitignore entries for mutants output directories - Add mutation-testing.md developer documentation - Write 9 mutation-killing tests for salience_recency.rs - Write 2 mutation-killing tests for recall_cache.rs Results (cargo-mutants v27.1.0): jaccard.rs: 12 mutants, 9 caught, 0 missed (100%) salience_recency: 53 mutants, 48 caught, 3 missed (96%) recall_cache: 25 mutants, 18 caught, 5 missed (80%) Refs: nginjen mutation testing pattern (#mutation-testing) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024) Two production bugs found by mutation testing: 1. Heading duplication: oversized markdown sections had their heading prepended twice in the first sub-chunk (once by split_by_headings, once more by the sub-chunk loop), corrupting downstream embeddings. Fixed by removing the dead heading_prefix re-prepend path entirely. 2. Multibyte infinite loop: split_long_text's zero-progress guard advanced by raw byte offsets that could land inside multi-byte UTF-8 characters (CJK/emoji), flooring back to start forever. chunk_markdown("日本語", 2) would hang. Fixed with a proper forward-char-boundary advance in the guard. Also: - 40 new mutation-killing tests (chunker: 20 → 60 tests) - cargo-mutants config moved to .cargo/mutants.toml (v27 schema: exclude_globs/exclude_re, valid keys only); glob paths fixed to match from workspace root; CLI update_check also excluded - 3 proven-equivalent mutants excluded with documented reasoning - docs/mutation-testing.md: final scores, bug postmortem, timeout notes Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.1 (#1025) Patch release: chunker heading duplication fix, multibyte infinite loop fix, mutation testing hardening (PR #1024). - Bump workspace version 0.14.0 -> 0.14.1 - Bump intra-workspace deps (cli/mcp/server -> core 0.14.1) - CHANGELOG entry for 0.14.1 Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(cli): uteke-cli crates.io publish failure + release verify step (#1030) * fix(cli): embed bundled assets inside crate root for crates.io publish include_str! paths pointed outside the package root (../../../.agents, ../../../extensions). cargo publish cannot bundle files outside the package, so uteke-cli has failed to publish since June (stuck at 0.4.3) while the release workflow hid the failure with continue-on-error. Assets now live in crates/uteke-cli/assets/ and ship inside the .crate. Verified locally: cargo package -p uteke-cli passes with full verify. * ci(release): verify crates.io versions after publish The continue-on-error on publish steps (added for the tag race fix) also hides genuine publish failures. Verify all four crates report the tagged version on crates.io; fail the job if any crate lags behind. * style: cargo fmt --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.2 (#1031) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * ci(release): reject stale RELEASE_NOTES.md (#1033) The v0.14.2 release reused the v0.13.0 notes because a committed RELEASE_NOTES.md is preferred over auto-generation and nothing checked which version it was for. The release shipped with wrong notes until manually fixed. The guard now requires the file to mention the tagged version. Also remove the stale v0.13.0 file so the next release auto-generates from the CHANGELOG unless someone writes fresh notes. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: HTTP & MCP recall strategy — default hybrid, loud validation (#1034, #1035) (#1038) * fix(server,mcp): default recall strategy to hybrid, validate strategy at boundary (#1034, #1035) HTTP: resolve strategy once (request > [recall] default_strategy config > hybrid). Invalid strategy returns 400 on all paths (bare recall, unified search, v1). The eager legacy recall that ran before strategy resolution is removed. Memory-only recall path routes through recall_hybrid with the same 3x over-fetch post-filter pattern used by recall_unified_memories (entity/category filters). MCP: uteke_recall schema exposes strategy (vector|fts5|hybrid|graph). Default resolves to hybrid, invalid values return a loud JSON-RPC error (-32603) instead of silently falling back to vector. Server startup: sanitize [recall] default_strategy from uteke.toml — invalid value warns and falls back to hybrid so a config typo cannot 400 every request with a message blaming the request. Docs: api-reference.md and mcp.md now describe the hybrid default, HTTP 400 on invalid, and the config fallback chain. Verified empirically against a scratch store: HTTP matrix 10/10, MCP harness 8/8 including engine parity (default == hybrid, warm cache) and loud bogus rejection. * docs: regenerate api-reference via docgen (strategy hybrid default, 400 on invalid) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.3 (#1039) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): release notes heredoc executed installer on runner — use template file (#1041) * fix(ci): replace unquoted heredoc in release notes generation with template file The unquoted heredoc << RELEASEEOF underwent shell expansion: markdown backticks became command substitutions and executed on the runner. v0.14.3 release: installer curl|sh ran, uteke-serve --port 8767 started and blocked the job for 48 minutes (2 runs, deterministic). Move the static tail (downloads table + quick start) to scripts/release-notes-template.md and substitute __VER__ via sed. Zero shell expansion over markdown content. Validated locally: generate completes instantly, output byte-correct. Fixes the release pipeline for all future releases. * ci: re-trigger review bots (LLM API transient error) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(docs): standardize recall latency to ~45ms 3 files still referenced ~30ms (stale from early benchmarks). Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md. Updated: architecture.md, mcp.md, index.md * chore: harden .gitignore + restructure repo for public safety - Add internal/ to .gitignore (prevents future strategy/launch doc leaks) - Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data) - Add docs/package-lock.json to .gitignore (VitePress build artifact) - Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs) - Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary) - Remove docs/package-lock.json from tracking Audit confirms: zero internal/ files in entire git history after filter-repo scrub. * feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015) * feat(bench): batch import + strategy flag + API embedding auto-config - Replace 53x individual remember calls with single JSONL batch import (10x speedup) - Add --strategy flag to pass vector|hybrid to uteke recall - Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars - Add --chunk-sessions flag for session chunking (Tier 2 prep) - Dedup recall results by session_id (first occurrence = highest rank) - Raise subprocess timeout 120s -> 600s for large imports Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp) * fix(bench): only mark sessions inserted after import succeeds Address Cora findings: - Move inserted_sids population to after batch import success - Parse import response to verify imported_count > 0 - Log warning if import returns 0 inserted * fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke (x86_64) instead of target/release/uteke (AArch64). Now resolves relative to repo root with shutil.which fallback. * docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100% Strategy comparison (uteke only): - Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810 - Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960 - Improvement: +12.6pp R@5 Hybrid uses RRF (k=60) fusion of vector + FTS5 search. 1 miss at R@5 (single-session-user), recovered at R@10. run_eval.py changes: - Add --strategy flag (vector|hybrid) - Throttle: taskset -c 0-1 + nice -n 19 - Absolute binary path resolution - Timeout: 900s per question * fix(core): add dedup, retry, and auto_link to import path (#1005) Import pipeline was missing 3 features that remember() has: 1. Dedup check — cosine >= 0.95 skips duplicate entries 2. Retry on embedding failure — 3 retries with backoff 3. Auto-link cosine edges — graph edges for imported memories Changes: - import_export.rs: call check_duplicate() before insert, use retry_embed() instead of single-attempt embed(), call auto_link_cosine() after successful insert - operations.rs: make retry_embed() and check_duplicate() pub(crate) so they're accessible from import_export.rs Import path is now semantically consistent with remember() path. * fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014) Add target_os = "android" to Linux .so branch and target_os = "ios" to macOS .dylib branch. Without these, uteke-core fails to compile for mobile targets with E0425 (cannot find value ORT_LIB_NAME). Android uses libonnxruntime.so (same as Linux, loaded via jniLibs). iOS uses libonnxruntime.dylib (same as macOS, framework embedded). This unblocks uteke-mobile cross-compilation. * feat(core): hybrid as default recall strategy Change default_strategy from vector to hybrid (RRF: vector + FTS5). Benchmark justification (LongMemEval-S): - Vector 500Q: R@5=85.4%, R@10=88.5% - Hybrid 50Q: R@5=98.0%, R@10=100.0% - Improvement: +12.6pp R@5 Changes: - config.rs: default_strategy = hybrid + assert in test - cli.rs: update help text default to hybrid - recall.rs: update comment to reflect new default - configuration.md: update all references - cli-reference.md: reorder examples, hybrid first as default Users who want vector-only: set default_strategy = vector in config or use --strategy vector flag. * fix(bench): guard taskset/nice with sys.platform check Cora finding: taskset is Linux-only, would FileNotFoundError on macOS. Now conditionally applied only when sys.platform == 'linux'. --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016) #1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set. Benchmark script sets this automatically, saving ~500ms per subprocess call. #1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary safety. #1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES. #1004: Already resolved — compute_graph_signals uses single batched SQL query. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017) * feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013) #1007: GET /lifecycle/deprecated — list deprecated memories with sunset info - list_deprecated() in aging.rs with DeprecatedMemoryInfo struct - Handler + route + API registry entry #1010: Memory tools guide injection - guide.rs module with default_guide() for system prompt injection - CLI: uteke guide command - API: GET /guide endpoint #1012: Implicit memory hierarchy docs - docs/organizing-memories.md — type + importance pattern - VitePress nav entry #1013: Auto-populate source provenance - CLI extraction: set source_label from input filename, source_type='extract' - Server extraction: set_source on each extracted fact - CLI output: display source_type alongside source in verbose mode * docs: regenerate api-reference.md for new endpoints (#1007, #1010) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat: scene-segmented extraction with priority scoring (#1009) (#1018) SYSTEM_PROMPT now requests scene-segmented JSON output with type and priority per fact. parse_facts handles three formats: scene-segmented nested JSON, flat object array with type/priority, and legacy flat string array (backward compatible). ExtractedFact struct carries content, scene, fact_type, priority. CLI and server callers use remember_typed + set_importance + scene tag. Offline mode unaffected — ExtractedFact::flat wraps existing strings. 9 new tests cover nested parsing, backward compat, dedup, priority range validation. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.0 (#1019) - Version bump to 0.14.0 - CHANGELOG entry for v0.14.0 - Docs: cli-reference scene-segmented extraction section - Docs: memory-lifecycle deprecated endpoint Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: crates.io publish race condition + uteke-mcp metadata (#1021) - Add 60s/30s/30s delay between crate publishes for index propagation - Add repository, rust-version, documentation, homepage to uteke-mcp - Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index - Fixes: 'manifest has no documentation, homepage or repository' warning Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): switch deploy-website from npm to bun (#1022) Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking and added it to .gitignore. The deploy-website workflow still referenced it via cache-dependency-path, causing setup-node to fail: 'Some specified paths were not resolved, unable to cache dependencies.' Fix: - Replace actions/setup-node with oven-sh/setup-bun - Replace npm ci with bun install --frozen-lockfile - Replace npm run build with bun run build - Add docs/bun.lock as tracked lockfile - Remove docs/package-lock.json from .gitignore (no longer relevant) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * feat(test): adopt cargo-mutants for mutation testing (#1023) Add mutation testing infrastructure to validate test quality across critical pure-logic modules. 11 new mutation-killing tests improve salience_recency.rs score from 76% to 96%. Changes: - Add cargo-mutants config (mutants.toml) with exclusions for modules requiring external services - Add mutation-testing CI workflow (develop→main PRs only, pre-release quality gate) - Add .gitignore entries for mutants output directories - Add mutation-testing.md developer documentation - Write 9 mutation-killing tests for salience_recency.rs - Write 2 mutation-killing tests for recall_cache.rs Results (cargo-mutants v27.1.0): jaccard.rs: 12 mutants, 9 caught, 0 missed (100%) salience_recency: 53 mutants, 48 caught, 3 missed (96%) recall_cache: 25 mutants, 18 caught, 5 missed (80%) Refs: nginjen mutation testing pattern (#mutation-testing) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024) Two production bugs found by mutation testing: 1. Heading duplication: oversized markdown sections had their heading prepended twice in the first sub-chunk (once by split_by_headings, once more by the sub-chunk loop), corrupting downstream embeddings. Fixed by removing the dead heading_prefix re-prepend path entirely. 2. Multibyte infinite loop: split_long_text's zero-progress guard advanced by raw byte offsets that could land inside multi-byte UTF-8 characters (CJK/emoji), flooring back to start forever. chunk_markdown("日本語", 2) would hang. Fixed with a proper forward-char-boundary advance in the guard. Also: - 40 new mutation-killing tests (chunker: 20 → 60 tests) - cargo-mutants config moved to .cargo/mutants.toml (v27 schema: exclude_globs/exclude_re, valid keys only); glob paths fixed to match from workspace root; CLI update_check also excluded - 3 proven-equivalent mutants excluded with documented reasoning - docs/mutation-testing.md: final scores, bug postmortem, timeout notes Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.1 (#1025) Patch release: chunker heading duplication fix, multibyte infinite loop fix, mutation testing hardening (PR #1024). - Bump workspace version 0.14.0 -> 0.14.1 - Bump intra-workspace deps (cli/mcp/server -> core 0.14.1) - CHANGELOG entry for 0.14.1 Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(cli): uteke-cli crates.io publish failure + release verify step (#1030) * fix(cli): embed bundled assets inside crate root for crates.io publish include_str! paths pointed outside the package root (../../../.agents, ../../../extensions). cargo publish cannot bundle files outside the package, so uteke-cli has failed to publish since June (stuck at 0.4.3) while the release workflow hid the failure with continue-on-error. Assets now live in crates/uteke-cli/assets/ and ship inside the .crate. Verified locally: cargo package -p uteke-cli passes with full verify. * ci(release): verify crates.io versions after publish The continue-on-error on publish steps (added for the tag race fix) also hides genuine publish failures. Verify all four crates report the tagged version on crates.io; fail the job if any crate lags behind. * style: cargo fmt --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.2 (#1031) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * ci(release): reject stale RELEASE_NOTES.md (#1033) The v0.14.2 release reused the v0.13.0 notes because a committed RELEASE_NOTES.md is preferred over auto-generation and nothing checked which version it was for. The release shipped with wrong notes until manually fixed. The guard now requires the file to mention the tagged version. Also remove the stale v0.13.0 file so the next release auto-generates from the CHANGELOG unless someone writes fresh notes. Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix: HTTP & MCP recall strategy — default hybrid, loud validation (#1034, #1035) (#1038) * fix(server,mcp): default recall strategy to hybrid, validate strategy at boundary (#1034, #1035) HTTP: resolve strategy once (request > [recall] default_strategy config > hybrid). Invalid strategy returns 400 on all paths (bare recall, unified search, v1). The eager legacy recall that ran before strategy resolution is removed. Memory-only recall path routes through recall_hybrid with the same 3x over-fetch post-filter pattern used by recall_unified_memories (entity/category filters). MCP: uteke_recall schema exposes strategy (vector|fts5|hybrid|graph). Default resolves to hybrid, invalid values return a loud JSON-RPC error (-32603) instead of silently falling back to vector. Server startup: sanitize [recall] default_strategy from uteke.toml — invalid value warns and falls back to hybrid so a config typo cannot 400 every request with a message blaming the request. Docs: api-reference.md and mcp.md now describe the hybrid default, HTTP 400 on invalid, and the config fallback chain. Verified empirically against a scratch store: HTTP matrix 10/10, MCP harness 8/8 including engine parity (default == hybrid, warm cache) and loud bogus rejection. * docs: regenerate api-reference via docgen (strategy hybrid default, 400 on invalid) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.14.3 (#1039) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * fix(ci): release notes heredoc executed installer on runner — use template file (#1041) * fix(ci): replace unquoted heredoc in release notes generation with template file The unquoted heredoc << RELEASEEOF underwent shell expansion: markdown backticks became command substitutions and executed on the runner. v0.14.3 release: installer curl|sh ran, uteke-serve --port 8767 started and blocked the job for 48 minutes (2 runs, deterministic). Move the static tail (downloads table + quick start) to scripts/release-notes-template.md and substitute __VER__ via sed. Zero shell expansion over markdown content. Validated locally: generate completes instantly, output byte-correct. Fixes the release pipeline for all future releases. * ci: re-trigger review bots (LLM API transient error) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * docs: sync stale version refs, roadmap v0.13–v0.14, merge comparison pages, fix release-notes asset names (#1055) * docs: sync stale version refs, roadmap v0.13–v0.14, merge comparison pages, fix release-notes asset names (#1043) - README/README.id/AGENT.md/install.md: version + test-count updates (0.14.3, 530+ tests); AGENT.md now lists all 5 workspace crates incl. docgen - docs/roadmap.md: add v0.13.0–v0.13.2 and v0.14.0–v0.14.3 entries from CHANGELOG - comparison: merge comparison-2026.md (canonical long-form) into comparison.md, keep at-a-glance matrix + extraction table from the old page, drop dead /blog link - extensions/hermes-memory-provider → extensions/hermes-uteke-memory (+ embedded assets copy and all path refs) — dir name no longer references the removed Mode B provider - scripts/release-notes-template.md: download table filenames get v prefix to match actual release assets (#1043) * fix(release): keep v prefix on pinned-install example in release notes template Co-authored-by: VIVAAN-DHAWAN <VIVAAN-DHAWAN@users.noreply.github.com> Same fix as #1046 — UTEKE_VERSION is used verbatim as the release tag by install.sh, so the pin example needs the v prefix too. Picking it up here so #1043 closes fully through this PR once the rest of the audit lands. * Revert "fix(release): keep v prefix on pinned-install example in release notes template" This reverts commit 222d067. * docs(agent): Critical Rule #14 — explicit approval before execution (#1056) Agents are advisory-mode by default: audit/check requests are not authorization to mutate. Gate push/merge/cherry-pick from others' PRs, comments/edits on others' PRs/issues, PR retargeting, and anything irreversible or externally visible behind explicit maintainer approval. Correct flow: analyze → present options → wait → execute approved scope → report. Real incident 2026-08-17 documented as cautionary example. * test: use ORT_LIB_NAME in find_ort_in_dir exact-match test (#1054) (#1059) Test hardcoded libonnxruntime.so; on macOS ORT_LIB_NAME is libonnxruntime.dylib so the exact-match path returned None and the assertion panicked. Use the platform constant instead. * chore: switch ID generation new_v4 → now_v7 (#1058) (#1060) All id call sites across uteke-core (remember, chunks, graph, edges, timeline, orphans, import) now mint UUIDv7. Storage and wire format unchanged (TEXT uuid); v4/v7 coexist — existing stores open unchanged. Adds two tests: v7 version nibble + ascending order for consecutive mints, and v4/v7 parse coexistence. * fix: soft-forgotten memories leak into list, search, and doctor counts (#1047) (#1061) Root cause was NOT a partial delete — soft_forget() correctly marks the row deprecated and removes the vector. The leak was read-side: - list()/search_content() never filtered deprecated rows → ghosts in 'uteke list' after forget - store.count(None) counted deprecated rows → doctor/verify reported permanent DB/Index MISMATCH after any soft-forget - load_all() (repair/verify source) included deprecated rows → repair() re-added soft-deleted vectors to the index, resurrecting them in recall Fix, uniform active-only contract: - list() (all 4 branches) + search_content(): AND deprecated = 0 - load_all(): AND deprecated = 0 (recompute_importance also stops wasting cycles on hidden rows) - count(): active-only for both None and Some(ns) paths — directly comparable to index.len() in doctor/verify - new count_all(): explicit include-deprecated totals for reporting Regression test: isolated temp-dir store (':memory:' stores resolve the vector index to a CWD file and cross-contaminate parallel test runs — that shared uteke_index.usearch pollution is also why this bug hid). Asserts soft-forgotten id absent from list/list-ns/load_all and doctor reports no MISMATCH. * fix: search_content(None) coerced to default namespace — cross-namespace keyword misses (#1051) (#1062) uteke_search / CLI search go through store.search_content() (LIKE substring), which mapped namespace=None to the 'default' namespace instead of searching across all namespaces like list(None) does (#526). Memories stored in any other namespace were invisible to keyword search unless the caller explicitly passed that namespace. - namespace=None now searches across all namespaces (deprecated rows still excluded) - hyphenated identifiers keep working as literal substrings on the LIKE path (pinned by test), and the FTS5 phrase path already handles them (unicode61 adjacency) — probed both in-test Regression test: cross-namespace hit via None, scoped hit via Some(ns), full and partial hyphenated identifier matching. * fix: recall cache hit skips salience/recency boosts — cold/warm score parity (#1037) (#1063) recall_hybrid() applied salience/recency boosts only on the cache-miss path; the cache-hit early return handed back raw cached scores. Same query + strategy returned different scores cold vs warm (delta ~0.13). Three-part fix: - Cache-hit read path now re-applies boosts (boost → sort → truncate → min_score), identical post-processing to the miss path via shared apply_salience_recency_boosts() helper. Cache intentionally stores RAW scores — boosts are time-dependent and re-applied per read. - Boost window: cache stores limit*4+16 candidates computed with min_score=0, so boosts can lift a memory from outside the raw top-N into the final top-N on warm reads too (cora finding — cached top-limit-only set would permanently exclude boostable candidates). - min_score thresholding now happens AFTER boosts on BOTH paths (previously miss-path Vector/Fts5 filtered raw scores while hit-path filtered boosted scores). Tests: cold/warm score parity (fails with max delta 0.125 pre-fix); boost-reorder-across-limit; noop-config warm hits respect limit. * fix: /export drops namespace attribution + deprecated-row delta undocumented (#1036) (#1064) ExportEntry had no namespace field — every exported row lost its namespace, so export→import collapsed multi-namespace stores into one. Also root-caused the reporter's 36-row delta: export() reads load_all() which filters deprecated=0 (soft-deleted rows excluded by design, but undocumented). - ExportEntry gains 'namespace' (serde default 'default' keeps old export files importable) - export() serializes m.namespace on every row - import(): caller-supplied namespace = explicit override for all rows; without it, per-row namespace wins → round-trips reconstruct namespaces - docgen/api-reference regenerated (no route change, freshness check) Test: 3-namespace × 2-row export shape (every row carries its ns), parse-side attribution preservation, legacy-row default fallback. * fix(mcp): uteke_dream destructive defaults — dry-run first, scope or confirm to apply (#1050) (#1065) A no-args uteke_dream call ran the maintenance pipeline with dry_run=false against ALL namespaces while the tool description said 'Safe to run periodically'. Real incident: single exploratory call mutated 4,067 rows store-wide. - dry_run defaults to TRUE — a no-args call can only preview - Applying requires explicit dry_run=false - Unscoped applying runs refused unless confirm_large=true (two-flag decision for whole-store maintenance) - Large-batch guard: applying runs projecting >100 changes refuse without confirm_large=true; preview computed first, so the refusal reports the projected count - Output announces scope ([SCOPE: ALL NAMESPACES]) on both preview and apply paths - Description rewritten: states destructive nature + dry-run-first guidance instead of 'Safe to run periodically' - docs/mcp.md tool table updated * feat(mcp): enrich tool outputs agents need — stats tiers/namespaces, doc_search scores, room ids (#1052) (#1066) - uteke_stats: multi-line output — tiers (hot/warm/cold), pinned + deprecated split, recall cache hits/misses, and per-namespace breakdown when unscoped (was: single 'Total | Tags | DB' line) - uteke_doc_search: per-result score, matched chunk heading + 120-char snippet (ranking was invisible; results were bare slug — title) - uteke_room_memories: lines now include 8-char short id so the next tool call (pin/forget/graph edges) can act on the row - uteke_room_recall description states the existing-room_id precondition instead of failing only at call time - core: Store::count_pinned() + Uteke::{count_pinned, count_deprecated, namespace_counts} accessors (store field is private; MCP uses the public API surface) * feat(mcp): uteke_get + uteke_update tools; short-ID resolution for all ID-taking tools (#1048, #1049) (#1067) MCP printed 8-char short ids in recall/list but every ID-consuming tool required the full UUID (exact SQL match) — the basic loop recall → act silently no-opped. resolve_id_prefix() existed in core (#794); MCP never wired it. - resolve_id() helper: full UUID passes through; any shorter prefix resolves via resolve_id_prefix(); ambiguous prefixes error loudly; unknown prefixes error instead of silent not-found - Wired into uteke_forget, uteke_pin, uteke_unpin, uteke_graph_add_edge, uteke_graph_remove_edge - NEW uteke_get {id} — full record by id/prefix, no truncation (fills the read-by-id gap; recall/search return ranked excerpts) - NEW uteke_update {id, content?, tags?, metadata?, importance?, pinned?, memory_type?} — partial-update semantics matching HTTP PUT /memory (#659); content change re-embeds - Tool schemas + docs/mcp.md: id params documented as UUID-or-prefix Tests (5, scratch temp-dir stores): prefix + full + unknown resolution; uteke_get full record via short id; pin via short id; forget short id happy path + bogus prefix errors loudly; update partial fields leave content untouched. * feat: structural export — full-store round-trip (rooms, graph, edges, documents, timeline) (#1068) * feat: structural export — full-store round-trip (memories, rooms, graph, edges, documents, chunks, timeline) (#1057) uteke export only dumped the memories table; rooms, knowledge graph, memory edges, documents (+chunks) and timeline evaporated on machine-to-machine migration and cloud→OSS exit. Format (structural-v1): single NDJSON stream — manifest first line {"uteke_export":{format_version, section counts}} + tagged rows ({"type":"room"|"graph_node"|...}). Every line a JSON object — filterable with standard ndjson tooling; no zip/tar dependency. - export_full(): all 10 sections, ids verbatim, deprecated memories excluded (policy documented); embeddings dropped (portable) - import_full(): SINGLE TRANSACTION (atomic — failure rolls back the whole restore), two-pass dependency order (memories before FK junctions, documents before chunks, nodes before edges), INSERT OR IGNORE (idempotent re-import), FTS rebuilt after restore, unknown tags ignored (forward-compatible) - restored memories get NULL embeddings — load_all() already filters NULL from index builds (#992) and repair --reembed targets exactly these rows; result reports needs_reembed count - CLI: uteke export --full; uteke import auto-detects the manifest line and routes to the structural path (legacy JSONL unchanged) - cli-reference documented (previously the export format was entirely undocumented) Round-trip test: 2 memories + room + link + memory edge + document + chunk → export → fresh store → import → all sections restored with original ids (room points at restored memory id), second import is a no-op. * fix: chunk created_at missing from structural dump SELECT (code-scanning #684) The document_chunks SELECT dropped created_at when the dump block was restructured; import bound unwrap_or("") so every restored chunk got a silently-empty timestamp. Column restored on both sides; round-trip test now asserts chunk created_at verbatim. * fix: preserve exported source_type on structural import (code-scanning #686) Dump wrote source_type verbatim but import hardcoded 'import' — restored memories lost 'user'/'document' provenance. Bind the exported value; 'import' remains the fallback for rows lacking it. * feat: supersession workflow — mark stale decisions superseded, flagged at recall (#1069) * feat: supersession workflow — mark stale decisions superseded, flagged at recall (#1053) Long-lived rooms accumulate revisited decisions that stay indistinguishable from current truth. The graph infra existed (typed memory_edges since v8) but was never a recall-layer concern. - EDGE_SUPERSEDED_BY type; Uteke::supersede(old,new,reason): wires the edge pair (old→new superseded_by, new→old supersedes) AND soft-deprecates the old memory in ONE transaction — partial failure rolls back entirely; recall cache invalidated for the namespace - Uteke::supersession_of(id): resolves the current-replacement pointer - MCP uteke_supersede {old_id,new_id,reason?} — accepts UUID or unambiguous prefix (resolve_id), self/unknown ids rejected loudly - MCP uteke_recall output flags stale hits: '⚠ superseded by <id> — verify before acting' (covers legacy/hard-restored deprecated rows that still surface) - docs/mcp.md updated Tests: edge pair + deprecation + hide-from-list + self/unknown rejection (core); short-id supersede via MCP executor. * test: nanos-precision scratch dirs — WAL busy race under parallel workspace runs * fix: re-supersession stacks edges + nondeterministic pointer lookup (code-scanning #687) Superseding the same memory twice left both superseded_by pairs (INSERT OR IGNORE) and supersession_of() picked LIMIT 1 with no ORDER BY. Now: prior pair deleted in-transaction before the new pair; lookup orders by created_at DESC deterministically. Test pins re-supersession replacing the pointer with no stacking. * ci: add source-branch-check — PRs into main must come from develop (#1072) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * chore: bump version to 0.15.0 (#1073) Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> * ci: source-branch-check allows chore/release-* release branches (#1075) * ci: source-branch-check allows chore/release-* release branches * ci: guard source-branch check on BASE_REF=main explicitly --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com> --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…1145) * chore(release): v0.16.0 — develop content into main (squash, conflicts resolved: take develop) * fix(core): remove duplicate capacity() from release squash conflict resolution (vecq compile) * fix(bench): sha1 -> sha256 for dataset fingerprint (scanner false positive, non-security use) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…hmarks, bundled skill bump (#1156) (#1158) * docs: refresh README (EN+ID) for 0.16.0 — fusion default, 500Q benchmarks, new features * docs: align strategy/version references with 0.16.0 defaults (fusion, install pins) * docs(roadmap): record v0.15.0 and v0.16.0 as released * docs: correct 500Q results-availability claim (aggregates in repo, raw on volume) * chore(skill): bump bundled uteke-memory-skill to 0.16.0 (fusion default, lifecycle, onboard) --------- Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(docs): standardize recall latency to ~45ms
3 files still referenced ~30ms (stale from early benchmarks).
Actual measured P50 at 1K-10K memories is ~45ms per benchmarks.md.
Updated: architecture.md, mcp.md, index.md
* chore: harden .gitignore + restructure repo for public safety
- Add internal/ to .gitignore (prevents future strategy/launch doc leaks)
- Add benchmarks/longmemeval/results_*/ to .gitignore (ephemeral data)
- Add docs/package-lock.json to .gitignore (VitePress build artifact)
- Move blog/comparison-2026.md → docs/comparison-2026.md (consolidate docs)
- Remove stale benchmarks/longmemeval/results_diverse/ (raw output, not summary)
- Remove docs/package-lock.json from tracking
Audit confirms: zero internal/ files in entire git history after filter-repo scrub.
* feat: hybrid as default recall strategy + import fixes (#1005, #1014) (#1015)
* feat(bench): batch import + strategy flag + API embedding auto-config
- Replace 53x individual remember calls with single JSONL batch import (10x speedup)
- Add --strategy flag to pass vector|hybrid to uteke recall
- Auto-configure embedding API from EMBED_API_KEY/EMBED_API_BASE env vars
- Add --chunk-sessions flag for session chunking (Tier 2 prep)
- Dedup recall results by session_id (first occurrence = highest rank)
- Raise subprocess timeout 120s -> 600s for large imports
Initial 5Q results: hybrid R@5=1.000 vs vector R@5=0.800 (+20pp)
* fix(bench): only mark sessions inserted after import succeeds
Address Cora findings:
- Move inserted_sids population to after batch import success
- Parse import response to verify imported_count > 0
- Log warning if import returns 0 inserted
* fix(bench): resolve uteke binary by absolute path to avoid x86_64 PATH clash
Background subprocess calls were resolving to /opt/data/.cargo/bin/uteke
(x86_64) instead of target/release/uteke (AArch64). Now resolves relative
to repo root with shutil.which fallback.
* docs(bench): add 50Q hybrid results — R@5 98.0%, R@10 100%
Strategy comparison (uteke only):
- Vector 500Q: R@5=85.4%, R@10=88.5%, NDCG@5=0.810
- Hybrid 50Q: R@5=98.0%, R@10=100%, NDCG@5=0.960
- Improvement: +12.6pp R@5
Hybrid uses RRF (k=60) fusion of vector + FTS5 search.
1 miss at R@5 (single-session-user), recovered at R@10.
run_eval.py changes:
- Add --strategy flag (vector|hybrid)
- Throttle: taskset -c 0-1 + nice -n 19
- Absolute binary path resolution
- Timeout: 900s per question
* fix(core): add dedup, retry, and auto_link to import path (#1005)
Import pipeline was missing 3 features that remember() has:
1. Dedup check — cosine >= 0.95 skips duplicate entries
2. Retry on embedding failure — 3 retries with backoff
3. Auto-link cosine edges — graph edges for imported memories
Changes:
- import_export.rs: call check_duplicate() before insert, use
retry_embed() instead of single-attempt embed(), call
auto_link_cosine() after successful insert
- operations.rs: make retry_embed() and check_duplicate() pub(crate)
so they're accessible from import_export.rs
Import path is now semantically consistent with remember() path.
* fix(core): cross-compile ORT_LIB_NAME for Android/iOS (#1014)
Add target_os = "android" to Linux .so branch and target_os = "ios"
to macOS .dylib branch. Without these, uteke-core fails to compile
for mobile targets with E0425 (cannot find value ORT_LIB_NAME).
Android uses libonnxruntime.so (same as Linux, loaded via jniLibs).
iOS uses libonnxruntime.dylib (same as macOS, framework embedded).
This unblocks uteke-mobile cross-compilation.
* feat(core): hybrid as default recall strategy
Change default_strategy from vector to hybrid (RRF: vector + FTS5).
Benchmark justification (LongMemEval-S):
- Vector 500Q: R@5=85.4%, R@10=88.5%
- Hybrid 50Q: R@5=98.0%, R@10=100.0%
- Improvement: +12.6pp R@5
Changes:
- config.rs: default_strategy = hybrid + assert in test
- cli.rs: update help text default to hybrid
- recall.rs: update comment to reflect new default
- configuration.md: update all references
- cli-reference.md: reorder examples, hybrid first as default
Users who want vector-only: set default_strategy = vector in config
or use --strategy vector flag.
* fix(bench): guard taskset/nice with sys.platform check
Cora finding: taskset is Linux-only, would FileNotFoundError on macOS.
Now conditionally applied only when sys.platform == 'linux'.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* perf: Phase 2 — skip update check in batch mode, pre-truncate embedding text (#1006, #1002) (#1016)
#1006: CLI update check skipped when UTEKE_NO_UPDATE_CHECK env var is set.
Benchmark script sets this automatically, saving ~500ms per subprocess call.
#1002: Pre-truncate text before tokenizer to avoid wasted CPU on tokens
beyond MAX_SEQ_LEN (2048). Uses 4 chars/token heuristic with char-boundary
safety.
#1003: Already resolved — idx_memories_namespace index exists in SCHEMA_INDEXES.
#1004: Already resolved — compute_graph_signals uses single batched SQL query.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: Phase 3 — lifecycle/deprecated, memory tools guide, source provenance (#1007, #1010, #1012, #1013) (#1017)
* feat: Phase 3 — lifecycle/deprecated endpoint, memory tools guide, source provenance (#1007, #1010, #1012, #1013)
#1007: GET /lifecycle/deprecated — list deprecated memories with sunset info
- list_deprecated() in aging.rs with DeprecatedMemoryInfo struct
- Handler + route + API registry entry
#1010: Memory tools guide injection
- guide.rs module with default_guide() for system prompt injection
- CLI: uteke guide command
- API: GET /guide endpoint
#1012: Implicit memory hierarchy docs
- docs/organizing-memories.md — type + importance pattern
- VitePress nav entry
#1013: Auto-populate source provenance
- CLI extraction: set source_label from input filename, source_type='extract'
- Server extraction: set_source on each extracted fact
- CLI output: display source_type alongside source in verbose mode
* docs: regenerate api-reference.md for new endpoints (#1007, #1010)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: scene-segmented extraction with priority scoring (#1009) (#1018)
SYSTEM_PROMPT now requests scene-segmented JSON output with type and
priority per fact. parse_facts handles three formats: scene-segmented
nested JSON, flat object array with type/priority, and legacy flat
string array (backward compatible).
ExtractedFact struct carries content, scene, fact_type, priority.
CLI and server callers use remember_typed + set_importance + scene tag.
Offline mode unaffected — ExtractedFact::flat wraps existing strings.
9 new tests cover nested parsing, backward compat, dedup, priority
range validation.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: bump version to 0.14.0 (#1019)
- Version bump to 0.14.0
- CHANGELOG entry for v0.14.0
- Docs: cli-reference scene-segmented extraction section
- Docs: memory-lifecycle deprecated endpoint
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix: crates.io publish race condition + uteke-mcp metadata (#1021)
- Add 60s/30s/30s delay between crate publishes for index propagation
- Add repository, rust-version, documentation, homepage to uteke-mcp
- Fixes: uteke-mcp/cli/server fail because uteke-core not yet in index
- Fixes: 'manifest has no documentation, homepage or repository' warning
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(ci): switch deploy-website from npm to bun (#1022)
Root cause: commit 0b5cad3 removed docs/package-lock.json from git tracking
and added it to .gitignore. The deploy-website workflow still referenced it
via cache-dependency-path, causing setup-node to fail:
'Some specified paths were not resolved, unable to cache dependencies.'
Fix:
- Replace actions/setup-node with oven-sh/setup-bun
- Replace npm ci with bun install --frozen-lockfile
- Replace npm run build with bun run build
- Add docs/bun.lock as tracked lockfile
- Remove docs/package-lock.json from .gitignore (no longer relevant)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(test): adopt cargo-mutants for mutation testing (#1023)
Add mutation testing infrastructure to validate test quality across
critical pure-logic modules. 11 new mutation-killing tests improve
salience_recency.rs score from 76% to 96%.
Changes:
- Add cargo-mutants config (mutants.toml) with exclusions for
modules requiring external services
- Add mutation-testing CI workflow (develop→main PRs only,
pre-release quality gate)
- Add .gitignore entries for mutants output directories
- Add mutation-testing.md developer documentation
- Write 9 mutation-killing tests for salience_recency.rs
- Write 2 mutation-killing tests for recall_cache.rs
Results (cargo-mutants v27.1.0):
jaccard.rs: 12 mutants, 9 caught, 0 missed (100%)
salience_recency: 53 mutants, 48 caught, 3 missed (96%)
recall_cache: 25 mutants, 18 caught, 5 missed (80%)
Refs: nginjen mutation testing pattern (#mutation-testing)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(chunker): heading duplication + multibyte infinite loop; 40 mutation-killing tests (score 50%→97%) (#1024)
Two production bugs found by mutation testing:
1. Heading duplication: oversized markdown sections had their heading
prepended twice in the first sub-chunk (once by split_by_headings,
once more by the sub-chunk loop), corrupting downstream embeddings.
Fixed by removing the dead heading_prefix re-prepend path entirely.
2. Multibyte infinite loop: split_long_text's zero-progress guard
advanced by raw byte offsets that could land inside multi-byte UTF-8
characters (CJK/emoji), flooring back to start forever.
chunk_markdown("日本語", 2) would hang. Fixed with a proper
forward-char-boundary advance in the guard.
Also:
- 40 new mutation-killing tests (chunker: 20 → 60 tests)
- cargo-mutants config moved to .cargo/mutants.toml (v27 schema:
exclude_globs/exclude_re, valid keys only); glob paths fixed to
match from workspace root; CLI update_check also excluded
- 3 proven-equivalent mutants excluded with documented reasoning
- docs/mutation-testing.md: final scores, bug postmortem, timeout notes
Verify run: 164 mutants, 149 caught, 0 missed, 5 timeout, 10 unviable (97%)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: bump version to 0.14.1 (#1025)
Patch release: chunker heading duplication fix, multibyte infinite
loop fix, mutation testing hardening (PR #1024).
- Bump workspace version 0.14.0 -> 0.14.1
- Bump intra-workspace deps (cli/mcp/server -> core 0.14.1)
- CHANGELOG entry for 0.14.1
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(cli): uteke-cli crates.io publish failure + release verify step (#1030)
* fix(cli): embed bundled assets inside crate root for crates.io publish
include_str! paths pointed outside the package root (../../../.agents,
../../../extensions). cargo publish cannot bundle files outside the
package, so uteke-cli has failed to publish since June (stuck at 0.4.3)
while the release workflow hid the failure with continue-on-error.
Assets now live in crates/uteke-cli/assets/ and ship inside the .crate.
Verified locally: cargo package -p uteke-cli passes with full verify.
* ci(release): verify crates.io versions after publish
The continue-on-error on publish steps (added for the tag race fix)
also hides genuine publish failures. Verify all four crates report the
tagged version on crates.io; fail the job if any crate lags behind.
* style: cargo fmt
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: bump version to 0.14.2 (#1031)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* ci(release): reject stale RELEASE_NOTES.md (#1033)
The v0.14.2 release reused the v0.13.0 notes because a committed
RELEASE_NOTES.md is preferred over auto-generation and nothing checked
which version it was for. The release shipped with wrong notes until
manually fixed.
The guard now requires the file to mention the tagged version. Also
remove the stale v0.13.0 file so the next release auto-generates from
the CHANGELOG unless someone writes fresh notes.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix: HTTP & MCP recall strategy — default hybrid, loud validation (#1034, #1035) (#1038)
* fix(server,mcp): default recall strategy to hybrid, validate strategy at boundary (#1034, #1035)
HTTP: resolve strategy once (request > [recall] default_strategy config > hybrid). Invalid strategy returns 400 on all paths (bare recall, unified search, v1). The eager legacy recall that ran before strategy resolution is removed. Memory-only recall path routes through recall_hybrid with the same 3x over-fetch post-filter pattern used by recall_unified_memories (entity/category filters).
MCP: uteke_recall schema exposes strategy (vector|fts5|hybrid|graph). Default resolves to hybrid, invalid values return a loud JSON-RPC error (-32603) instead of silently falling back to vector.
Server startup: sanitize [recall] default_strategy from uteke.toml — invalid value warns and falls back to hybrid so a config typo cannot 400 every request with a message blaming the request.
Docs: api-reference.md and mcp.md now describe the hybrid default, HTTP 400 on invalid, and the config fallback chain.
Verified empirically against a scratch store: HTTP matrix 10/10, MCP harness 8/8 including engine parity (default == hybrid, warm cache) and loud bogus rejection.
* docs: regenerate api-reference via docgen (strategy hybrid default, 400 on invalid)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: bump version to 0.14.3 (#1039)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(ci): release notes heredoc executed installer on runner — use template file (#1041)
* fix(ci): replace unquoted heredoc in release notes generation with template file
The unquoted heredoc << RELEASEEOF underwent shell expansion: markdown
backticks became command substitutions and executed on the runner.
v0.14.3 release: installer curl|sh ran, uteke-serve --port 8767 started
and blocked the job for 48 minutes (2 runs, deterministic).
Move the static tail (downloads table + quick start) to
scripts/release-notes-template.md and substitute __VER__ via sed.
Zero shell expansion over markdown content. Validated locally: generate
completes instantly, output byte-correct.
Fixes the release pipeline for all future releases.
* ci: re-trigger review bots (LLM API transient error)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* docs: sync stale version refs, roadmap v0.13–v0.14, merge comparison pages, fix release-notes asset names (#1055)
* docs: sync stale version refs, roadmap v0.13–v0.14, merge comparison pages, fix release-notes asset names (#1043)
- README/README.id/AGENT.md/install.md: version + test-count updates (0.14.3, 530+ tests); AGENT.md now lists all 5 workspace crates incl. docgen
- docs/roadmap.md: add v0.13.0–v0.13.2 and v0.14.0–v0.14.3 entries from CHANGELOG
- comparison: merge comparison-2026.md (canonical long-form) into comparison.md, keep at-a-glance matrix + extraction table from the old page, drop dead /blog link
- extensions/hermes-memory-provider → extensions/hermes-uteke-memory (+ embedded assets copy and all path refs) — dir name no longer references the removed Mode B provider
- scripts/release-notes-template.md: download table filenames get v prefix to match actual release assets (#1043)
* fix(release): keep v prefix on pinned-install example in release notes template
Co-authored-by: VIVAAN-DHAWAN <VIVAAN-DHAWAN@users.noreply.github.com>
Same fix as #1046 — UTEKE_VERSION is used verbatim as the release tag by
install.sh, so the pin example needs the v prefix too. Picking it up here so
#1043 closes fully through this PR once the rest of the audit lands.
* Revert "fix(release): keep v prefix on pinned-install example in release notes template"
This reverts commit 222d06740a9988745ae47569d9cbba461cd2a492.
* docs(agent): Critical Rule #14 — explicit approval before execution (#1056)
Agents are advisory-mode by default: audit/check requests are not
authorization to mutate. Gate push/merge/cherry-pick from others' PRs,
comments/edits on others' PRs/issues, PR retargeting, and anything
irreversible or externally visible behind explicit maintainer approval.
Correct flow: analyze → present options → wait → execute approved scope
→ report. Real incident 2026-08-17 documented as cautionary example.
* test: use ORT_LIB_NAME in find_ort_in_dir exact-match test (#1054) (#1059)
Test hardcoded libonnxruntime.so; on macOS ORT_LIB_NAME is
libonnxruntime.dylib so the exact-match path returned None and the
assertion panicked. Use the platform constant instead.
* chore: switch ID generation new_v4 → now_v7 (#1058) (#1060)
All id call sites across uteke-core (remember, chunks, graph, edges,
timeline, orphans, import) now mint UUIDv7. Storage and wire format
unchanged (TEXT uuid); v4/v7 coexist — existing stores open unchanged.
Adds two tests: v7 version nibble + ascending order for consecutive
mints, and v4/v7 parse coexistence.
* fix: soft-forgotten memories leak into list, search, and doctor counts (#1047) (#1061)
Root cause was NOT a partial delete — soft_forget() correctly marks the
row deprecated and removes the vector. The leak was read-side:
- list()/search_content() never filtered deprecated rows → ghosts in
'uteke list' after forget
- store.count(None) counted deprecated rows → doctor/verify reported
permanent DB/Index MISMATCH after any soft-forget
- load_all() (repair/verify source) included deprecated rows → repair()
re-added soft-deleted vectors to the index, resurrecting them in recall
Fix, uniform active-only contract:
- list() (all 4 branches) + search_content(): AND deprecated = 0
- load_all(): AND deprecated = 0 (recompute_importance also stops
wasting cycles on hidden rows)
- count(): active-only for both None and Some(ns) paths — directly
comparable to index.len() in doctor/verify
- new count_all(): explicit include-deprecated totals for reporting
Regression test: isolated temp-dir store (':memory:' stores resolve the
vector index to a CWD file and cross-contaminate parallel test runs —
that shared uteke_index.usearch pollution is also why this bug hid).
Asserts soft-forgotten id absent from list/list-ns/load_all and doctor
reports no MISMATCH.
* fix: search_content(None) coerced to default namespace — cross-namespace keyword misses (#1051) (#1062)
uteke_search / CLI search go through store.search_content() (LIKE
substring), which mapped namespace=None to the 'default' namespace
instead of searching across all namespaces like list(None) does (#526).
Memories stored in any other namespace were invisible to keyword
search unless the caller explicitly passed that namespace.
- namespace=None now searches across all namespaces (deprecated rows
still excluded)
- hyphenated identifiers keep working as literal substrings on the
LIKE path (pinned by test), and the FTS5 phrase path already handles
them (unicode61 adjacency) — probed both in-test
Regression test: cross-namespace hit via None, scoped hit via Some(ns),
full and partial hyphenated identifier matching.
* fix: recall cache hit skips salience/recency boosts — cold/warm score parity (#1037) (#1063)
recall_hybrid() applied salience/recency boosts only on the cache-miss
path; the cache-hit early return handed back raw cached scores. Same
query + strategy returned different scores cold vs warm (delta ~0.13).
Three-part fix:
- Cache-hit read path now re-applies boosts (boost → sort → truncate →
min_score), identical post-processing to the miss path via shared
apply_salience_recency_boosts() helper. Cache intentionally stores
RAW scores — boosts are time-dependent and re-applied per read.
- Boost window: cache stores limit*4+16 candidates computed with
min_score=0, so boosts can lift a memory from outside the raw top-N
into the final top-N on warm reads too (cora finding — cached
top-limit-only set would permanently exclude boostable candidates).
- min_score thresholding now happens AFTER boosts on BOTH paths
(previously miss-path Vector/Fts5 filtered raw scores while hit-path
filtered boosted scores).
Tests: cold/warm score parity (fails with max delta 0.125 pre-fix);
boost-reorder-across-limit; noop-config warm hits respect limit.
* fix: /export drops namespace attribution + deprecated-row delta undocumented (#1036) (#1064)
ExportEntry had no namespace field — every exported row lost its
namespace, so export→import collapsed multi-namespace stores into one.
Also root-caused the reporter's 36-row delta: export() reads load_all()
which filters deprecated=0 (soft-deleted rows excluded by design, but
undocumented).
- ExportEntry gains 'namespace' (serde default 'default' keeps old
export files importable)
- export() serializes m.namespace on every row
- import(): caller-supplied namespace = explicit override for all rows;
without it, per-row namespace wins → round-trips reconstruct
namespaces
- docgen/api-reference regenerated (no route change, freshness check)
Test: 3-namespace × 2-row export shape (every row carries its ns),
parse-side attribution preservation, legacy-row default fallback.
* fix(mcp): uteke_dream destructive defaults — dry-run first, scope or confirm to apply (#1050) (#1065)
A no-args uteke_dream call ran the maintenance pipeline with
dry_run=false against ALL namespaces while the tool description said
'Safe to run periodically'. Real incident: single exploratory call
mutated 4,067 rows store-wide.
- dry_run defaults to TRUE — a no-args call can only preview
- Applying requires explicit dry_run=false
- Unscoped applying runs refused unless confirm_large=true (two-flag
decision for whole-store maintenance)
- Large-batch guard: applying runs projecting >100 changes refuse
without confirm_large=true; preview computed first, so the refusal
reports the projected count
- Output announces scope ([SCOPE: ALL NAMESPACES]) on both preview and
apply paths
- Description rewritten: states destructive nature + dry-run-first
guidance instead of 'Safe to run periodically'
- docs/mcp.md tool table updated
* feat(mcp): enrich tool outputs agents need — stats tiers/namespaces, doc_search scores, room ids (#1052) (#1066)
- uteke_stats: multi-line output — tiers (hot/warm/cold), pinned +
deprecated split, recall cache hits/misses, and per-namespace
breakdown when unscoped (was: single 'Total | Tags | DB' line)
- uteke_doc_search: per-result score, matched chunk heading + 120-char
snippet (ranking was invisible; results were bare slug — title)
- uteke_room_memories: lines now include 8-char short id so the next
tool call (pin/forget/graph edges) can act on the row
- uteke_room_recall description states the existing-room_id
precondition instead of failing only at call time
- core: Store::count_pinned() + Uteke::{count_pinned, count_deprecated,
namespace_counts} accessors (store field is private; MCP uses the
public API surface)
* feat(mcp): uteke_get + uteke_update tools; short-ID resolution for all ID-taking tools (#1048, #1049) (#1067)
MCP printed 8-char short ids in recall/list but every ID-consuming tool
required the full UUID (exact SQL match) — the basic loop recall → act
silently no-opped. resolve_id_prefix() existed in core (#794); MCP
never wired it.
- resolve_id() helper: full UUID passes through; any shorter prefix
resolves via resolve_id_prefix(); ambiguous prefixes error loudly;
unknown prefixes error instead of silent not-found
- Wired into uteke_forget, uteke_pin, uteke_unpin,
uteke_graph_add_edge, uteke_graph_remove_edge
- NEW uteke_get {id} — full record by id/prefix, no truncation (fills
the read-by-id gap; recall/search return ranked excerpts)
- NEW uteke_update {id, content?, tags?, metadata?, importance?,
pinned?, memory_type?} — partial-update semantics matching HTTP PUT
/memory (#659); content change re-embeds
- Tool schemas + docs/mcp.md: id params documented as UUID-or-prefix
Tests (5, scratch temp-dir stores): prefix + full + unknown resolution;
uteke_get full record via short id; pin via short id; forget short id
happy path + bogus prefix errors loudly; update partial fields leave
content untouched.
* feat: structural export — full-store round-trip (rooms, graph, edges, documents, timeline) (#1068)
* feat: structural export — full-store round-trip (memories, rooms, graph, edges, documents, chunks, timeline) (#1057)
uteke export only dumped the memories table; rooms, knowledge graph,
memory edges, documents (+chunks) and timeline evaporated on
machine-to-machine migration and cloud→OSS exit.
Format (structural-v1): single NDJSON stream — manifest first line
{"uteke_export":{format_version, section counts}} + tagged rows
({"type":"room"|"graph_node"|...}). Every line a JSON object —
filterable with standard ndjson tooling; no zip/tar dependency.
- export_full(): all 10 sections, ids verbatim, deprecated memories
excluded (policy documented); embeddings dropped (portable)
- import_full(): SINGLE TRANSACTION (atomic — failure rolls back the
whole restore), two-pass dependency order (memories before FK
junctions, documents before chunks, nodes before edges),
INSERT OR IGNORE (idempotent re-import), FTS rebuilt after restore,
unknown tags ignored (forward-compatible)
- restored memories get NULL embeddings — load_all() already filters
NULL from index builds (#992) and repair --reembed targets exactly
these rows; result reports needs_reembed count
- CLI: uteke export --full; uteke import auto-detects the manifest
line and routes to the structural path (legacy JSONL unchanged)
- cli-reference documented (previously the export format was entirely
undocumented)
Round-trip test: 2 memories + room + link + memory edge + document +
chunk → export → fresh store → import → all sections restored with
original ids (room points at restored memory id), second import is a
no-op.
* fix: chunk created_at missing from structural dump SELECT (code-scanning #684)
The document_chunks SELECT dropped created_at when the dump block was
restructured; import bound unwrap_or("") so every restored chunk got a
silently-empty timestamp. Column restored on both sides; round-trip
test now asserts chunk created_at verbatim.
* fix: preserve exported source_type on structural import (code-scanning #686)
Dump wrote source_type verbatim but import hardcoded 'import' —
restored memories lost 'user'/'document' provenance. Bind the exported
value; 'import' remains the fallback for rows lacking it.
* feat: supersession workflow — mark stale decisions superseded, flagged at recall (#1069)
* feat: supersession workflow — mark stale decisions superseded, flagged at recall (#1053)
Long-lived rooms accumulate revisited decisions that stay
indistinguishable from current truth. The graph infra existed (typed
memory_edges since v8) but was never a recall-layer concern.
- EDGE_SUPERSEDED_BY type; Uteke::supersede(old,new,reason): wires the
edge pair (old→new superseded_by, new→old supersedes) AND
soft-deprecates the old memory in ONE transaction — partial failure
rolls back entirely; recall cache invalidated for the namespace
- Uteke::supersession_of(id): resolves the current-replacement pointer
- MCP uteke_supersede {old_id,new_id,reason?} — accepts UUID or
unambiguous prefix (resolve_id), self/unknown ids rejected loudly
- MCP uteke_recall output flags stale hits: '⚠ superseded by <id> —
verify before acting' (covers legacy/hard-restored deprecated rows
that still surface)
- docs/mcp.md updated
Tests: edge pair + deprecation + hide-from-list + self/unknown
rejection (core); short-id supersede via MCP executor.
* test: nanos-precision scratch dirs — WAL busy race under parallel workspace runs
* fix: re-supersession stacks edges + nondeterministic pointer lookup (code-scanning #687)
Superseding the same memory twice left both superseded_by pairs
(INSERT OR IGNORE) and supersession_of() picked LIMIT 1 with no ORDER
BY. Now: prior pair deleted in-transaction before the new pair;
lookup orders by created_at DESC deterministically. Test pins
re-supersession replacing the pointer with no stacking.
* ci: add source-branch-check — PRs into main must come from develop (#1072)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: bump version to 0.15.0 (#1073)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* ci: source-branch-check allows chore/release-* release branches (#1075)
* ci: source-branch-check allows chore/release-* release branches
* ci: guard source-branch check on BASE_REF=main explicitly
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* ci: bump actions/download-artifact v4 -> v8 (Node 24) (#1080)
Fixes #1079. Node.js 20 is deprecated on GitHub-hosted runners;
download-artifact v8 targets Node 24. upload-artifact already at v7.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): structural import silently dropped room_documents + harden roundtrip test (#1078 batch 1) (#1081)
- import: route room_document rows to a pending pass restored AFTER
documents (FK doc_slug order was wrong; INSERT OR IGNORE hid the drop)
- export/import: include room_documents.added_at (NOT NULL, no default;
missing column made every insert a silent no-op)
- remove dead match arms document/document_chunk superseded by pass-2
- test: seed all 10 sections, assert manifest + imported counts, chunk
created_at and pinned flag round-trip verbatim, idempotent re-import
- mutants: 36/36 caught (was 19 missed)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(ci): skip CLA check for bots (dependabot, renovate, github-actions) (#1077)
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): add author_type (human|agent) to memory records (#1084)
* feat(core): add author_type (human|agent) to memory records (#1083)
Schema v16: ALTER TABLE with DEFAULT 'agent' + index, backfills existing
rows. Memory struct + validate_author_type. set_author_type() on Store and
Uteke facade (invalid values -> Validation error). Server /remember accepts
optional author_type, 400 on invalid. All memory SELECTs now include
source/source_type/author_type (previously dropped on read). Version
asserts in tests use CURRENT_SCHEMA_VERSION instead of hardcoded numbers.
* fix(server): validate author_type before write + regenerate API docs (#1083)
Cora review finding: /remember inserted the memory before validating
author_type, so an invalid value left a persisted record while the client
got a 400 (partial-write). Validation now runs before any insert.
Regenerates docs/api-reference.md (API Docs Fresh check).
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(server): wire 'at' time-travel param into /room/recall (#1085)
* fix(server): wire 'at' time-travel param into /room/recall (#1082)
RoomRecallRequest now accepts 'at' (RFC3339). Invalid timestamps
return 400 instead of being silently dropped. Both recall paths
(chronological fallback + semantic) apply point-in-time filtering
using the same temporal rules as core recall_at_time: created_at <= at,
valid_from <= at, valid_until > at, not deprecated.
* fix(server): over-fetch before temporal filter in room recall chronological path
Cora MAJOR on #1085: SQL LIMIT truncated candidates before the
point-in-time post-filter, returning fewer results than requested.
Fetch 3x (min limit+10) when 'at' is set, then truncate after filtering.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* docs(server): document deprecation time-travel limitation (#1086) (#1087)
Cora review on #1085 flagged that deprecated memories are excluded
from time-travel regardless of when they were deprecated. The schema
lacks a deprecation timestamp, so this cannot be fixed without a
schema migration (tracked in #1086). Document the limitation at the
predicate until then.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): room semantic segmentation (LLM-free, #1088) (#1090)
* feat(core): add room semantic segmentation PoC (LLM-free, #1088)
Segment a room's memories by embedding-distance boundaries (LycheeMemory
V2, arXiv:2608.12990). Boundaries: adjacent cosine < threshold or max
segment size; short runs merge into the previous segment. Segments are
the batching unit for future segment-level LLM consolidation — one LLM
call per segment instead of per memory.
* fix(core): propagate store errors in room_segments instead of silent drops
* perf(core): replace per-memory fetch with single-query recall_room in room_segments
* fix(core): merge short runs into actual preceding segment in room_segments
Previously the merge target was derived from the raw segment index
(merged[start] - 1). When the preceding run had itself been merged
earlier in the pass, this produced orphan indices, leaving tiny
segments behind and defeating min_size for chains of short runs.
Now targets merged[start - 1]. Adds regression test.
* test(core): use orthogonal embeddings in chain-merge regression test
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): expose semantic segments in room_summary (#1088) (#1091)
Uteke::room_summary now enriches the store's tag-based summary with
embedding-distance segments when room memories have embeddings. Segments
are the batching units for future segment-level LLM consolidation
(LycheeMemory V2 granularity). No-embedding rooms keep segments=None
(serde-optional, backward compatible). room_segments split into a
public fetch + room_segments_inner to avoid double queries.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): segment-level consolidation planner, measure-only (#1088) (#1092)
Adds room_consolidation_plan(): builds per-segment LLM batching plans
from the semantic segmentation (LycheeMemory V2) with call/token cost
estimates — zero LLM calls. Validated on clone prod DB: large rooms
show 67-91% call reduction vs naive per-memory consolidation.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): provenance trust policy for consolidation (#1089) (#1093)
Adds provenance module: TrustTier classification from author/source types,
non-amplification rule (consolidated record capped at weakest source tier),
and a hedging guard rejecting confidence-upgrading rewrites (EN + ID markers).
Policy layer for the LLM consolidation executor; validated via unit tests.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): LLM consolidation executor reusing extraction setup (#1088) (#1094)
* feat(core): LLM consolidation executor reusing extraction setup (#1088)
Adds consolidation_exec: executes a segment-based ConsolidationPlan via the
same OpenAI-compatible endpoint as the import/extraction pipeline (shared
ExtractionConfig - one setup, many uses). Each batch becomes one LLM call
with a consolidation prompt that preserves hedging. Outputs pass the #1089
provenance gate before write; sources are soft-deprecated, never deleted.
Budget-capped via max_llm_calls.
* fix(core): isolate per-batch LLM failures in consolidation executor
A failed LLM call no longer aborts execute_plan mid-run: the error is
recorded in ConsolidationExecution.batch_errors and remaining batches
still process. Failed batches leave their sources untouched.
* fix(core): embed consolidated records and isolate store failures per batch
- Compute embedding via ConsolidationStore::embed_content before insert so
consolidated records are visible to vector recall (was embedding: vec![]).
- insert/deprecate failures abort only the affected batch and are recorded
in batch_errors; the run continues (duplicate content from a partial
write is recoverable by dedup, lost batches are not).
* fix(core): budget guard counts LLM requests made, not successes
Failed calls still consume an API request against max_llm_calls, so the
guard now uses a dedicated llm_calls_made counter incremented before the
call regardless of outcome. Also rewords an error message that tripped a
false-positive SQL-injection pattern (no SQL involved).
* fix(core): LLM client timeout + whitelist LLM-provided memory_type
- reqwest blocking client gets a 120s timeout so a slow consolidation
endpoint cannot hang the run (parity with update_check).
- LLM-provided fact_type is validated against the allowed set
(fact/decision/preference/procedure/context); anything else falls back
to "fact" so arbitrary model output can never set an unknown type.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: wire consolidation pipeline into core, CLI, and HTTP (#1088) (#1095)
* feat: wire consolidation pipeline into core, CLI, and HTTP (#1088)
- Implement ConsolidationStore for Uteke (SQLite store + lazy embedder)
- Add embed_text/add_to_index helpers on Uteke for the write path
- CLI: uteke room consolidate <room> [--apply] [--max-calls N] (dry-run default)
- HTTP: POST /room/consolidate (dry-run default; apply gated on extraction
LLM config with 503; hard budget cap)
- Register endpoint in API registry; document CLI + API in docs/
- Tests: trait dispatch, plan batching, deprecate+insert write paths
* fix: make insert_memory rollback-safe (review #1095)
Cora review flagged non-atomic insert: if add_to_index or
link_memory_to_room failed, the row stayed persisted but un-searchable
and/or unlinked. Now both failure paths compensate by removing the
index entry and deleting the row before returning the error. Added
remove_from_index helper for the compensating action.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore(docs): regenerate api-reference via docgen (#1095 follow-up) (#1096)
The manual consolidate endpoint entry was placed in the wrong spot and
used hand-written content; docgen now renders it from the API registry
(including the #1088 related-tag).
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): vecq pure-Rust vector backend behind feature flag (#1099)
* feat(core): add vecq pure-Rust vector backend behind feature flag
Add an alternative vector index backend built on vecq-core (training-free
4-bit vector quantization, pure Rust). This removes the C++ toolchain
requirement for mobile/FFI builds and unblocks uteke-mobile#39.
- Make 'usearch' an optional feature (still in default)
- Add 'vecq' feature, mutually exclusive with 'usearch' (compile_error guard)
- VectorIndex public API unchanged; internals dispatch via cfg
- vecq tombstones: removed rows filtered via key map (no incremental delete)
- vecq search over-fetches k + dead-row count to compensate tombstones
- New CI job 'vecq-check' runs vecq-backend unit tests on every PR
- Unit tests pass on both backends (usearch: 7, vecq: 8 incl. tombstone test)
Closes #1098
* fix(core): derive vecq row key from physical row count
Address cora review findings on the vecq backend:
- CRITICAL: derive the vecq row key from `index.len()` (physical append-only
row count) instead of the key-mapping sidecar's `next_key`. If the sidecar
were stale relative to the index file after a crash, insert would map keys
to the wrong physical rows and search would return wrong memories.
- MAJOR: over-fetch by the exact dead-row count (index.len() - live keys)
instead of next_key-derived estimate, so search returns up to k live
results even with tombstones.
* fix(core): handle phantom sidecar keys on vecq insert
Address cora PR review on #1099: if a crash happens between the `.keys`
sidecar write and the index file write, the reloaded sidecar can hold
keys >= index.len() (phantom keys with no physical row). Insert now
derives the vecq key from the physical row count and explicitly removes
any phantom key at that position (warn + drop its id mapping) before
claiming it — the phantom's row never existed, so nothing live can be
shadowed.
* fix(core): key allocation and usearch add in one cfg block
The previous refactor split key allocation and index.add() into separate
cfg blocks, and the add() path derived the key via next_key - 1 — wrong
whenever insert is called on a fresh index whose next_key counter is
still 0 (CI failure in test_forget_then_reinsert: duplicate key 0).
Allocation, map updates, capacity growth, and add() now live in a single
usearch block so the key variable is used directly.
* fix(core): validate vecq embedding dims before mutating key maps
Address cora PR review on #1099: the dimension check ran after the key
maps were updated, so a dimension-mismatch error left key_to_id holding
a key whose row was never added — corrupting len() accounting and
aliasing the next insert's key. The check now runs at the top of
insert(), before any state mutation.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): track deprecated_at for correct time-travel recall (#1086) (#1100)
* fix(core): track deprecated_at for correct time-travel recall (#1086)
- Schema v17: add deprecated_at column, backfill legacy deprecated rows
- Set deprecated_at on all deprecate paths, clear on restore
- recall_at_time: memory deprecated AFTER the point-in-time now appears
(previously any deprecated flag excluded it); deprecated BEFORE is excluded
- memory_exists_at handler: same temporal predicate
- recall_inner(include_deprecated) so temporal path sees deprecated rows
- Regression test (no ONNX dependency)
* fix(server): align NULL deprecated_at semantics with core (#1086); drop stray embed_cache.db
* test(server): align at_time tests with deprecated_at semantics (#1086)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore: remove embed_cache.db artifact and ignore it (#1101)
* chore: remove embed_cache.db artifact and ignore it
* test(core): make #1086 regression test embedder-free (pure predicate)
* refactor(core): extract shared memory_existed_at predicate for recall_at_time (#1086)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat: per-pair dedup control via POST /consolidate/pair (#1076) (#1102)
* feat: per-pair dedup control via POST /consolidate/pair (#1076)
Add caller-chosen survivor consolidation: keep id_keep untouched,
deprecate (or hard-delete) id_remove. Reuses the same soft-delete +
reason + index cleanup path as bulk /consolidate, overriding the
hard-coded keep-newer rule when the reviewer picks the richer entry.
* fix(core): ephemeral index & embed cache for in-memory stores
rusqlite reports an empty string (not ":memory:") as the path of an
in-memory database, so store.path() leaked through the ephemeral guard:
every ':memory:' open wrote ./uteke_index.usearch and ./embed_cache.db
into the CWD. Concurrent tests then raced on one shared index file
(sidecar keys vs usearch keys) producing non-deterministic
'Duplicate keys not allowed' flakes.
Treat both '' and ':memory:' as ephemeral: no index file, no embed
cache file. Verified: 3x full workspace suite green with zero stray
files emitted.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(ci): keep v prefix in release notes download table + pinned install (#1043) (#1103)
* fix(ci): keep v prefix in release notes download table + pinned install (#1043)
The release job substitutes __VER__ in release-notes-template.md with
${VER#v} — the tag WITHOUT its v prefix — while the archived assets are
named with it (`<artifact>-v<semver>.tar.gz`). Copy-pasting a filename
or the pinned-install line from the notes therefore 404s.
- release.yml: substitute __VER__ with the full tag (v kept)
- template: drop the literal v from the 5 download-table rows (the tag
now supplies it) so rendered names stay exactly as before
- ci.yml: new Release Notes Template job — renders the template with a
placeholder semver and asserts the names equal the workflow's
`<artifact>-v<ver>.<ext>` pattern, and that the pinned-install line
carries the v-prefixed tag. Catches this class of regression before
a tag is cut, not after.
Verified locally: rendered names diff clean against the actual
v0.15.0 release assets; the new CI step passes verbatim.
Reimplements the intent of #1046 (closed: wrong base branch, author
unresponsive) — thanks @VIVAAN-DHAWAN for the original report.
Closes #1043
* ci: restructure release-notes check to single-line printf
CodeCora flagged the multi-line quoted string in the new check: YAML
block scalars happen to strip the leading indentation today, but the
pattern is fragile — any re-indent silently changes the expected value.
Replace with printf + line continuations (indentation after a
continuation backslash is ignored by sh), same expected set.
Verified locally: extracted step verbatim from the YAML, passes.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(server): add --version/-V flag to uteke-serve (#1044) (#1104)
* feat(server): add --version/-V flag to uteke-serve (#1044)
uteke-serve had no way to report its version while the main CLI does.
Add a --version / -V arm to the arg parser printing
`uteke-serve <CARGO_PKG_VERSION>` (mirroring the CLI's format), list it
in --help, and cover it with integration tests via CARGO_BIN_EXE
(long flag, short flag, and unknown-flag still rejected with a usage
hint).
Reimplements the intent of #1045 (closed: wrong base branch, author
unresponsive) — thanks @VIVAAN-DHAWAN for the original patch.
Closes #1044
* test(core): merge racing ORT_LIB_PATH env tests into one sequential test
resolve_ort_lib_prefers_env_var and resolve_ort_lib_errors_on_missing_
env_var_path both mutated the process-global ORT_LIB_PATH in parallel
#[test]s. When the invalid-path test overwrote the var between the
valid test's set_var and resolve, resolve_ort_lib() returned Err and
the is_ok() assert failed — a timing-dependent CI flake (seen on this
branch's first CI run; passed locally, failed in CI).
Merged both cases into a single #[test] that runs them back-to-back,
eliminating the shared-state race. 5x targeted + 3x full workspace
suite green.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(cli): honor UTEKE_HOME as store path override (#1105) (#1107)
The CLI resolved its store from --store or uteke.toml only, ignoring
UTEKE_HOME — so an isolated-looking invocation silently opened the
default (often production) database. During v0.16.0 pre-release QA this
caused accidental v16+v17 schema migrations on a live DB.
Resolution order is now --store > UTEKE_HOME > uteke.toml > default,
via a shared resolve_store_path() used by both main and repair --rebuild
(the latter resolves index paths independently and is the most
destructive command to mis-target).
Verified empirically: UTEKE_HOME=<empty dir> uteke doctor now reports
DB=0 in that dir instead of the default store's 8045 memories.
Closes #1105
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix: echo author_type in remember responses + CLI --author-type flag (#1106) (#1108)
POST /remember and uteke remember --json stored author_type correctly
but never echoed it, so clients couldn't confirm what was recorded.
The CLI didn't even expose the field (#1084 shipped HTTP-only).
- HTTP /remember response now includes author_type (default mirrors
the schema default 'agent', #1083)
- CLI: new --author-type flag (human|agent), validated BEFORE any
write so invalid values fail loudly without leaving a stored row;
echoed in --json on both remember paths (contradiction + plain)
- run_via_server path forwards the flag in the HTTP body
Verified empirically in isolated stores: explicit human -> echo human
+ DB human; default -> echo agent + DB agent; invalid 'robot' -> loud
error / HTTP 400, zero rows written.
Closes #1106
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): repair() no longer evicts document chunk vectors from index (#1113)
* fix(core): repair() no longer evicts document chunk vectors from index
repair() rebuilt the vector index from load_all() which returns memories
only. Document chunk vectors ('chunk:<id>' keys) were silently dropped
from the index on every repair, degrading semantic doc search until the
document was re-upserted. doctor() reported consistency OK afterwards,
masking the eviction.
Add Store::load_all_chunk_embeddings() and include chunk vectors in the
rebuild (embeddings are already persisted in document_chunks, so no
re-embedding is needed). Regression test verifies semantic doc search
still finds chunks after repair.
Fixes #1110
* test: mark repair-chunks regression as requiring ONNX embedder
The test opens a real Uteke instance which eagerly initializes the ONNX
embedder; CI runners lack the ORT native library, so the test panicked
at open(). Marked #[ignore] with the same convention as the five
existing embedder-dependent tests; it still runs locally with the
release bundle via --ignored.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(build): vecq binaries now buildable via forwarded backend features (#1115)
uteke-cli, uteke-server, and uteke-mcp pulled uteke-core with default
features, so requesting vecq always unified with the default usearch
backend and hit the mutual-exclusion compile_error guard. The feature
was library-testable but unshippable as a binary, while CI stayed green
because the vecq-check job only ran 'cargo test --lib vector'.
Each downstream crate now declares 'usearch'/'vecq' features that
forward to uteke-core (default = usearch), and uteke-server forwards
its backend choice through its uteke-mcp dependency as well. The
resulting build matrix:
cargo build -p uteke-cli # usearch (default)
cargo build -p uteke-cli --no-default-features --features vecq
CI vecq-check now also builds all three binaries with the vecq feature
so packaging breaks fail the build, not just library tests.
Fixes #1109
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): backend-aware index filename, doctor label, forget --confirm help (#1116)
* fix(core): backend-aware index filename, doctor label, forget --confirm help
The vector index filename was hardcoded to 'uteke_index.usearch' in
Uteke::open, the doctor check, and the CLI bench/repair commands, even
when the binary was built with the vecq backend. A vecq build wrote
VECQ-format bytes into a '.usearch'-named file, and opening a store
with the other backend could parse-fail and leave the index file
deleted after a re-save (observed during #1112 E2E).
The filename now derives from INDEX_EXT, so each backend owns its own
file: 'uteke_index.vecq' for vecq builds, 'uteke_index.usearch' for
usearch builds. Cross-backend opens simply miss the file and start
fresh instead of clobbering the other format.
Also:
- doctor check label is now 'vecq index'/'usearch index' per backend
- 'uteke forget --confirm' help text explains it is the non-interactive
equivalent of the y/N prompt (docs item 4 of #1112)
Fixes #1112
* test: mark index-filename regression as requiring ONNX embedder
Uteke::open eagerly initializes the ONNX embedder; CI runners lack the
ORT native library, so the test panicked at open(). Same convention as
the existing embedder-dependent tests — runs locally via --ignored.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): verify/doctor count doc chunks on DB side, no false MISMATCH (#1114)
* fix(core): verify/doctor count doc chunks on DB side, no false MISMATCH
The vector index holds memories AND document chunk vectors
('chunk:<id>' keys), but verify()/doctor() compared index.len()
against a memories-only DB count. Any store with documents
reported MISMATCH and recommended repair — which (before #1110
fix) would evict the chunk vectors.
VerifyReport gains a chunk_count field (serde default, backward
compatible). Doctor and CLI verify output now show chunks
explicitly.
Fixes #1111
* test: mark verify-chunks regression as requiring ONNX embedder
Same as the repair-chunks regression: Uteke::open eagerly initializes
the ONNX embedder which CI runners lack. Runs locally via --ignored.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(cli): repair report compares against memories + chunks (#1117)
Follow-up to #1110/#1111: the CLI repair summary flagged a false
'Index count still differs from DB' warning for stores containing
documents, because it compared index_after (memories + chunk vectors)
against db_count (memories only). RepairReport now carries chunk_count
(serde default, backward-compatible) and the CLI compares against
db_count + chunk_count.
Caught during post-merge E2E retest: repair on a doc-only store kept
the chunk vector (search still found it) but printed a misleading
warning.
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* test(longmemeval): fast-eval foundation — datasets + Modal harness + 500Q metrics (#1124)
* bench(longmemeval): 500Q hybrid run results + reranker + fast-eval harness (#1118)
- Final metrics 500Q hybrid: recall@5=0.909, recall@10=0.964, ndcg@5=0.875
- reranker.py: cross-encoder ONNX (ms-marco-MiniLM-L-6) + --rerank flag di run_eval
- modal_fast50/pull_fast: fast-eval infra (Volume terpisah uteke-lmeval-fast)
- dataset eval cepat: fast50/fast10/temporal15/multisession15 (stratified)
- fix: uteke binary fallback (.local/bin) utk host ini
* bench(longmemeval): remove reranker from foundation PR (#1118 cancelled)
The foundation PR keeps only reusable infra: 500Q metrics, fast-eval
datasets (via deterministic generator), Modal harness, compare tooling.
The cross-encoder reranker is dropped per the single-model decision;
temporal boost (#1119) lands as its own PR on top.
* chore: retrigger CI (CodeCora review failed with LLM API error)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* test(longmemeval): temporal date-window boost (#1119) (#1126)
* bench(longmemeval): temporal date-window boost (#1119)
- temporal.py: temporal expression parser -> (lo,hi) window; patterns:
last-N, N-days/weeks-ago (digits+words), last week/month, between X-Y,
before/after month, in-month, this month/year
- boost_ranking(): additive RRF boost for in-window sessions
- run_eval.py: --temporal flag (default OFF) + --temporal-boost (0.0022)
- modal_fast50.py: bake temporal.py into image; LMEVAL_TEMPORAL=1 passthrough
- test_temporal.py: 21 unit tests (parser + boost math), all passing
* fix(longmemeval): propagate temporal flag to Modal workers + isolate volume cache
Two bugs in the A/B harness found before burning Modal credits:
- LMEVAL_TEMPORAL was read at module level inside the container, so the
env var never reached the worker: a --temporal run would silently
execute the baseline. The flag now travels through the shard spec.
- Shard cache keyed only by strategy: a temporal shard would resume
from the stale 470Q baseline volume entry. Volume paths are now
variant-keyed (hybrid vs hybrid_temporal).
* test(longmemeval): remove tautological assertions flagged by review
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* test(longmemeval): MMR diversity rerank flag + replay recording (#1120) (#1127)
* bench(longmemeval): MMR diversity rerank (#1120)
* fix(longmemeval): normalise MMR relevance scale + record session order in jsonl
* feat(longmemeval): mmr_lambda support in Modal harness for A/B evaluation
* fix(longmemeval): append mmr suffix to volume variant (preserve temporal key)
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(longmemeval): dataset-tagged volume paths + qid-set cache validation (#1129) (#1130)
* fix(longmemeval): dataset-tagged volume paths + qid-set cache validation (#1129)
Volume cache keyed only by (variant, shard_idx) let a fast50 shard satisfy
a multisession15 run's length check and be returned verbatim - silent
cross-dataset stale hits. Now the dataset sha1[:8] is part of the volume
path, and a completed-shard cache hit additionally requires the qid set
to match exactly. list_volume globs across tag subdirs.
* fix(longmemeval): drop truncated tail lines from volume shard cache (#1130)
Review finding: a partially-written tail line (killed container) raised
JSONDecodeError and aborted the shard. Skip malformed lines instead; the
qid-set equality check downstream still guards completeness.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* fix(core): make sha2 non-optional — no-default-features profile compiles again (#1131) (#1133)
* feat(bench): add RRF fusion mode (--fusion) to run_eval.py
Vector and hybrid strategies fail on DISJOINT question sets in fast50
offline simulation (7 questions vector-only wins, 5 hybrid wins, overlap
only in failures). Reciprocal Rank Fusion of both rankings captures
both: simulated R@5 0.9267 -> 0.9700, R@10 0.9700 -> 1.0000 on all 50
questions (weight plateau [1.3, 1.6], k=60).
Implementation: --fusion runs two recalls (primary strategy x1.5 +
complementary vector<->hybrid x1), RRF-fuses deduped session rankings
before temporal/MMR post-processing. Default OFF; no behavior change
without the flag.
Smoke test (hardest question gpt4_8279ba03, baseline R@5=0.00):
--strategy vector --fusion -> R@5=1.00, matches simulation exactly.
* feat(bench): --fusion flag passthrough in modal_fast50
run_shard spec gains 'fusion' key -> --fusion flag to run_eval.py, and
variant key gains '_fusion' suffix so fused shards never share volume
cache entries with plain strategy shards (#1129 pattern).
* fix(core): make sha2 non-optional — no-default-features profile compiles again (#1131)
The embedding cache (CachingEmbedder/EmbeddingCache) hashes keys with
SHA256 and is backend-agnostic — it wraps OpenAI/Ollama embedders too,
not just ONNX. But sha2 was optional behind the onnx feature while
embed/cache.rs imported it unconditionally, so every
--no-default-features build failed with E0432.
sha2 is tiny and pure-Rust; making it non-optional is the honest
dependency statement. Verified profiles: default (onnx+usearch),
onnx+vecq, vecq-only. CI vecq-check gains a no-onnx check step so the
profile stays green.
---------
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(core): Fusion as default recall strategy + benchmark tooling (v0.16.0) (#1135)
* docs(bench): tune fusion weight to vec×1.7 based on x86 rankings
x86 re-simulation using ACTUAL Modal rankings (results_modal_vector +
results_modal_hybrid_fresh_post1130) shows:
- vec×1.5: R@5=0.9600 (4 fails: gpt4_8279ba03, 60472f9c, 862b... etc)
- vec×1.7: R@5=0.9800 (3 fails, gpt4_8279ba03 fixed at rank≤5)
- plateau [1.7, 1.9] all → 0.9800
wv=1.7 chosen (mid-plateau, not edge). The default remains 1.5 in
run_eval.py; production runs pass --fusion-primary-weight 1.7.
* perf(bench): fusion default weight 1.5 -> 1.7 (x86 actual rankings)
Re-simulation on ACTUAL x86 Modal rankings (results_modal_vector +
results_modal_hybrid_fresh_post1130) instead of ARM-local rankings:
- wv=1.5: R@5=0.9600 (4 fails, incl. gpt4_8279ba03 at 0.0)
- wv=1.7: R@5=0.9800 (gpt4_8279boss rank pushed <=5)
- plateau [1.7, 1.9] -> 0.9800; 1.7 = mid-plateau
Also fixes stale help text (default: 1.7).
* fix(bench): fusion weight in volume cache key + --fusion-weight flag
The wv=1.7 confirm run silently returned the cached wv=1.5 shards:
variant key 'vector_fusion' was shared by both weights. Weight is now
part of the variant key (vector_fusion1.7 vs vector_fusion1.5), and
--fusion-weight passes the value through to run_eval so the cache key
matches what actually ran (#1129 pattern applied to fusion).
* feat(core): RecallStrategy::Fusion variant (parse/serde only)
Adds the Fusion strategy to the public enum with exact-match parsing
and kebab-case serde, plus roundtrip tests. Dispatch returns a loud
Validation error until the arm is wired in the follow-up commit —
never a silent fallback to another strategy.
Benchmark evidence (#1123): vector and hybrid fail on disjoint
question sets; weighted RRF fusion (vector x1.7 + hybrid x1, k=60)
reaches R@5 0.98 vs 0.9267 hybrid on LongMemEval fast50.
* refactor(core): extract compute_recall below the cache layer
The strategy match inside recall_hybrid moves verbatim into a private
compute_recall method. The dispatcher (cache check, cache put,
salience/recency boosts, truncation) is unchanged — pure refactor,
zero behavior change, full workspace tests green.
Fusion (#1123) will call compute_recall twice (Vector + Hybrid) and
fuse the rankings, bypassing the per-sub-call cache so the dispatcher
applies boosts exactly once.
* feat(core): Fusion strategy arm — vector×1.7 + hybrid×1 RRF (#1123)
Wires RecallStrategy::Fusion into compute_recall: runs the Vector and
Hybrid sub-strategies at boost_window depth and fuses their rankings
with the benchmark-tuned weights (vector x1.7, hybrid x1.0, k=60).
Sub-calls bypass the cache/boost layer; the dispatcher applies
salience/recency boosts exactly once and caches the fused set —
mirroring the harness semantics that measured fast50 R@5 0.98.
Includes rrf_fuse_weighted helper (unit-tested: overlap wins + dedup,
weight-driven ordering, empty inputs) and end-to-end integration
tests: on-topic memory ranks first, cold/warm cache parity, empty
store returns empty (no silent fallback, no error).
* feat(core)!: Fusion is the default recall strategy (#1123)
Moves #[default] from Hybrid to Fusion: every recall that does not
specify a strategy now uses the benchmark-proven weighted RRF fusion
(vector x1.7 + hybrid x1, k=60; fast50 R@5 0.98 vs 0.9267 hybrid).
Behavior change ONLY for implicit-default callers; explicit
strategy=default_strategy config is untouched. 0.x semver: minor
version bump (0.16.0) carries this change.
* feat(cli): default_strategy fusion + strategy list updated
- default_strategy: hybrid -> fusion (0.16.0, #1123)
- UTEKE_RECALL_STRATEGY env var accepts 'fusion'
- generated-config template and --help text list fusion
- default_recall_config test asserts fusion
Explicit [recall].default_strategy in user configs still wins via the
overlay merge — only the implicit default changes.
* feat(server): fusion default + strategy list in 400 message
Implicit fallback when a request omits strategy and uteke.toml has no
default_strategy: Hybrid -> Fusion (#1123, 0.16.0), matching the core
enum default and CLI default_strategy. The 400 message for invalid
strategy values now lists all five strategies. Request-level strategy
and config default_strategy semantics unchanged.
* feat(mcp): fusion default + schema enum
MCP recall tool schema gains 'fusion' (default since 0.16.0, #1123);
implicit fallback when the client omits strategy: Hybrid -> Fusion,
matching core/CLI/HTTP defaults. Invalid-strategy error lists all five
strategies. Explicit client strategy values are unchanged.
* feat(bench): 'fusion' as first-class strategy in harness
run_eval.py --strategy fusion exercises the IN-CORE default path
(uteke 0.16.0+ weighted RRF), distinct from the harness-level --fusion
flag which post-processes two CLI recalls. Enables the zero-config
validation run: --strategy fusion on a default-config binary.
* docs: fusion default + 0.16.0 changelog + version bump
- CHANGELOG 0.16.0: fusion strategy added, default hybrid -> fusion
(implicit-default callers only; explicit config untouched)
- docs/configuration.md: default_strategy fusion + env var table
- docs/benchmarks.md: fusion pipeline + fast50 numbers
- benchmarks/longmemeval/RESULTS.md: fusion run row, disjoint-failset
explanation, tuning evidence (plateau 1.7-1.9 on actual x86 rankings)
- workspace + crates 0.15.0 -> 0.16.0
* feat(bench): --strategy default sentinel for zero-config validation
'default' omits --strategy in the recall command so the binary's own
implicit default (0.16.0: fusion) is exercised — validates that a
fresh install with no config gets benchmark-grade recall out of the
box (#1123).
* docs(bench): zero-config validation results (local ARM, 0.16.0)
- Parity proven: implicit default == explicit fusion (identical top-10)
- Divergence from hybrid confirmed (default is not the old behavior)
- fast10 zero-config: R@5 0.9667, R@10 1.0, NDCG@5 0.9643
- 1 partial miss @5 (60472f9c) matches known hard question from
the Modal x86 fusion run (#1123)
* test(longmemeval): 3-way RRF simulation — fts5 arm adds nothing (no-go)
- collect fts5-only rankings 500Q via modal fanout (fix: bake temporal.py
+ mmr.py into image)
- 3-way grid best = 2-way shipped (0 flipped questions at any weight)
- fts5-only 500Q baseline R@5=0.8211 (matches harness 0.820)
- ranking-side optimization exhausted; next headroom is insert granularity
* fix(ci): ignore ONNX-dependent fusion tests in CI + regen api docs
- fusion_recall_finds_on_topic_memory / fusion_recall_empty_store_returns_empty
require the ONNX runtime lib which is u…
🔍 Cora AI Code Review✅ No issues found. Code looks good! Review powered by cora-code · BYOK · MIT |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two post-release housekeeping items:
mainback intodevelop(merge commit4517c85) — the 0.17.0 release merge (a2ec81a, chore(release): v0.17.0 → main #1197) lived only on main; this restores history parity so the next develop→main release PR is conflict-free. MANDATORY post-release step that was skipped on 0.17.0 and caused a conflict (release: v0.17.0 (develop → main) #1195/release: v0.17.0 → main (conflict-resolved branch) #1196 detour).AGENTS.md— codifies the branch/release rules as agent-facing docs: branch naming (feat/fix/docs/chore/...), main accepts PRs only from develop or chore/release-*, PR body headers (## What/## Why/## Testing), post-release sync requirement (this rule now written down), api-reference is generated, single workspace version.Why
Post-release main→develop sync is now the standard across all CodeCora repos; it was missed on 0.17.0 and cost a conflict detour. Codifying it in AGENTS.md (plus the branch rules that bit us this session: branch naming check, source-branch check) prevents a repeat.
Testing
4517c85= develop + main;git logshows parity (no cherry commits left)cargo test --workspaceunaffected (docs only) — CI on this PR validates