Skip to content

chore: release v0.21.1 to main - #1479

Merged
ajianaz merged 263 commits into
mainfrom
chore/release-v0.21.1-main
Oct 15, 2026
Merged

ajianaz merged 263 commits into
mainfrom
chore/release-v0.21.1-main

Conversation

@ajianaz

@ajianaz ajianaz commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

What

Release v0.21.1 (patch, approved by the owner): develop to main. The release branch is develop plus git merge -s ours origin/main, so the tree is exactly develop's. Proof that main carries nothing develop lacks: main's tree equals develop at de1c78a (the 0.21.0 release content).

Contents are in the CHANGELOG [0.21.1] section: uteke upgrade verifies a minisign signature (stable-only, no downgrade, fails closed), ORT_LIB_PATH directory fix for Homebrew installs, CLI logs on stderr so --json is clean, rustls 0.23.46, a dependabot batch, one named handler per server route group, and the release-signature reminder.

Why

0.21.0 Homebrew installs run without embeddings (#1459) and --json output is corrupted by log lines (#1461); 0.21.1 fixes both and is the first release that can carry the offline minisign signature (#1327).

Testing

  • Develop passed the full CI and merge gate for every PR in this release (release prep: chore(release): v0.21.1 - version bump, changelog, docs sync #1478).
  • Pre-release mutation gate: the standard gate selects 0 mutants for this diff (mutants.toml excludes embed/*.rs); a manual run over the changed ort_init.rs resolver caught 1 of 1.
  • git diff origin/develop HEAD of this branch is empty.
  • After the merge: tag, Release workflow, offline signing (scripts/sign-release.sh v0.21.1), tap PR, crates.io and Docker checks.

ajianaz and others added 30 commits August 14, 2026 22:07
…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>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
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>
, #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>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…plate 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>
…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.
…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.
…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.
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.
#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.
…ace 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.
… 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.
…umented (#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.
…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
…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)
…l 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.
… 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.
…d 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.
…1072)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* 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>
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>
… 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>
…s) (#1077)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* 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 (#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>
)

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): 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>
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>
ajianaz and others added 27 commits October 12, 2026 13:09
#1424)

Refs #1343

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…1343) (#1425)

Extract /remember, uteke_remember and the CLI-via-server body into
uteke-core's remember_request module (RememberInput, RememberPolicy,
RememberRequest), following the recall slice. No wire behaviour change.

Refs #1343

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…) (#1426)

Add uteke-core ForgetInput / ForgetPolicy / ForgetRequest and
resolve_memory_id: DELETE /forget query parsing, id-or-tag decoding, id
resolution (full UUID, prefix, ambiguity), the existence check and the
CLI-via-server URL now live in one module. HTTP /forget, MCP uteke_forget
(and the shared MCP resolve_id) and the CLI-via-server forget use it.
Extraction only: where the surfaces disagree, each surface's current
behaviour is a policy value; the read-only gate stays HTTP-only.

Refs #1343

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…1434)

Writing to a non-default namespace now carries a one-sentence deprecation
warning on HTTP (/remember, /room/remember, /extract, /import, /room/create,
PUT /memory), MCP (uteke_remember, uteke_update, uteke_room_create) and the
CLI (stderr only). The wording lives once in uteke-core; the server logs it
once per distinct namespace. Routing, storage and reads are unchanged.
Tool descriptions, uteke guide, skill and docs steer to rooms, tags, author.

Refs #1427

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
#1435) (#1436)

resolve_id_prefix now escapes %, _ and the escape character, so an id prefix is
never a pattern for any caller, and the HTTP forget policy validates the id like
MCP (hex digits and '-', 400 otherwise). Before, DELETE /forget?id=%25 matched
every memory whose id contains "25" and deleted it when exactly one matched.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
) (#1437)

The api_registry table is now consulted by the router (unregistered
method+path is a 404 before any handler runs) and by the read-only-token
gate (derived from a new per-row writes flag). The source-scanning
coverage test is replaced by tests built from the table.

Refs #1344

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…1438)

New "Reaching the server from another device on your LAN" section
(token first, bind address, Host header behaviour, plain-HTTP caveat, firewall,
Docker) and "Keeping the token out of the client config" in the MCP docs
(stdio needs no token, static bearer only, no OAuth in the self-hosted server).
Every claim was checked against a running server.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…#1442)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
AGENTS.md named the project room but did not require reading it before a task
or writing progress to it afterwards, so decisions and release state lived only
in whichever session made them. Spell out both steps, note that room recall is
cross-namespace, and that the shared store differs from a local CLI store.

Also record the owner rule that commits, PRs, comments and release notes carry
no AI attribution trailers or lines.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* security(mcp): per-tool read-only gate for callers without write access (#1439)

* security(mcp): refuse read-only tool calls as tool errors; pin the allowlist (#1439)

* security(server): read-only tokens reach MCP read tools; writes stay refused (#1439)

* docs: read-only tokens and MCP tools (#1439)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
… retry of validation errors (#1443)

* fix(core): retry_embed reports a missing embedder instead of panicking

* fix(core): do not retry embedding validation errors

* test(core): pin the missing-embedder error variant; doc wording

* fix(core): recall reports a missing embedder instead of panicking

ensure_embedder only returns Ok once the slot holds an embedder and nothing
resets it to None, so the expect was unreachable today. Return an Embedding
error instead so a future change cannot turn it into a panic. Adds a test that
vector recall on a store with no usable embedder errors rather than panics.

* fix(core): skip a missing RRF memory instead of panicking

recall_rrf fills rrf_scores and memories in the same loops, so the expect
could not fire today. Use filter_map and skip a missing id with a warning
instead of panicking if that invariant ever breaks. take(limit) stays before
the filter, so the normal path is unchanged.

* style(core): wrap the RRF skip warning and note the short-result case

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…1444)

* fix(cli): an explicit --store never routes through a running server

With [server] enabled = true and a server up, remember/recall/search/list/
stats/forget were sent to the server, which serves its own store, so an
explicit --store was silently ignored and data went to the wrong database
(the LongMemEval harness always passes --store). Server routing is now
decided by should_route_via_server(), which returns false whenever --store
is set; the cheap check runs before the network probe. UTEKE_HOME is
unchanged. (#1439 B4)

* test(bench): modal_fast50.py reads UTEKE_RELEASE like modal_fanout.py

* fix(cli): keep routing --store pointing at the default store; changelog

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* chore(release): v0.21.0 - version bump, changelog, docs sync

Prepared on chore/prepare-0.21.0; the release PR into main is opened from a chore/release-* branch after the mutation-test gate.

* docs(security): checksums are signed since v0.21.0, not since the release after v0.20.0

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* test(core): kill the mutants the 0.21.0 gate missed

Exact-value tests for the mutants the pre-release gate
(v0.20.1..develop, uteke-core) left alive:

- lib.rs: FallbackSettings::with_env_overrides -> Default::default()
- forget_request.rs: Display for ForgetRequestError -> Ok(Default::default())
- list_request.rs: Display for ListRequestError -> Ok(Default::default())
- remember_request.rs: Display for RememberRequestError -> Ok(Default::default())
- operations.rs recall_rrf: vector window `limit * 3` -> `limit + 3`
- operations.rs recall_rrf: vector window `limit * 3` -> `limit / 3`
- operations.rs recall_rrf: `score / max_rrf` -> `score * max_rrf`

* test(core): assert only the env var the fallback test owns

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…h) (#1457)

Replaces #1451 (toml 1.1.6 -> 1.1.8), #1452 (usearch 2.26.3 -> 2.26.4),
#1453 (uuid 1.27.0 -> 1.28.0) and #1454 (serde_json 1.0.151 -> 1.0.154).
The lockfile change equals the union of those four PRs.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
#1458)

* refactor(server): extract every route arm into a named handler (#1344)

route() keeps the shared preflight (version prefix, Host guard, CORS
preflight, auth, read-only gate, registry 404), then packs the per-request
state into a Call and dispatches with one line per route. Each former
match arm is now a function in a domain module under handlers/ (health,
memory, namespaces, tags, rooms, docs, graph, maintenance, mcp).

Pure move: arm order, the (method, path) set and the bodies are
unchanged apart from rustfmt reflow and dropping borrows that clippy
flags now that path and uteke are already references.

* test(server): the dispatch match and the registry carry the same routes (#1344)

every_registry_row_has_a_handler_arm only proves each row reaches a
handler. The new test reads the one-line-per-route dispatch match from
handlers/mod.rs and asserts its (method, path) set equals
api_registry::ENDPOINTS with no duplicates, so an arm without a row is
caught too. The registry's sync comment now points at handlers/mod.rs.

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…ide (#1459) (#1460)

The Homebrew shims exported the libexec directory as ORT_LIB_PATH. dlopen
fails on a directory, so every Homebrew install since v0.19.0 stored
memories without embeddings. Resolve a directory to the platform library
inside it, with a warning, and fail with a clear message when the library is
not there.

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* feat(cli): verify a minisign signature over the checksums in uteke upgrade (#1327)

Embed the release public key and verify checksums-sha256.txt.minisig over
the raw checksums bytes before trusting any hash. The trusted comment must
be 'uteke <tag>' so an older signed release cannot be replayed. A missing or
invalid signature is a hard failure; UTEKE_UPGRADE_SKIP_CHECKSUM skips it
too, with an updated warning.

* build(release): add offline minisign signing script and public key (#1327)

* docs: document minisign verification and the offline signing step (#1327)

* fix(cli): refuse to downgrade and verify the signature before the archive download (#1327)

Compare versions numerically (semver, pre-release aware) and refuse an older
release; unparseable tags are an error. Fetch and authenticate the checksums
before downloading the archive, through a fail-closed authenticate_checksums.
New fixtures: legacy signature, trailing-space and double-space trusted
comments (the throwaway key was regenerated; all fixtures re-signed).

* fix(release): verify the cosign bundle before signing the checksums (#1327)

* build(deps): keep Cargo.lock to the minisign-verify entries only (#1327)

* docs: state the signature is checked by uteke upgrade only, and the cosign pre-check (#1327)

* fix(cli): install stable releases only in uteke upgrade, derive the hash from verified checksums only (#1327)

Reject any non-stable target tag (pre-release, build metadata, malformed)
before the prompt and any download, with no override. A pre-release running
binary can still upgrade to a newer stable. The expected archive hash now
comes from verified_expected_hash, which authenticates the checksums first,
and is unit-tested. Adds the whitespace-only signature case.

* test(release): cover sign-release.sh failure paths with stub gh and cosign (#1327)

* docs: uteke upgrade installs stable releases only; sign-release.sh needs a cosign bundle (#1327)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…le (#1471)

* fix(cli): write console logs to stderr so --json output stays parseable (#1461)

The console layer had no writer and defaulted to stdout, so any WARN or ERROR
was printed in front of the command's own output and broke json.loads(stdout).
Send it to stderr; the file layer is unchanged.

* docs(changelog): record the stderr log fix (#1461)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…1327) (#1472)

* ci(release): detect whether the latest release checksums are signed (#1327)

* ci(release): say in the header that this part only detects (#1327)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
* ci(release): open, re-ping and close the reminder issue (#1327)

* ci(release): restore the full script header (#1327)

* ci(release): state the real effect of the 100-issue listing limit (#1327)

---------

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
…rs (#1327) (#1476)

Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
Co-authored-by: ajianaz <ajianaz@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

🔍 Cora AI Code Review

✅ No issues found. Code looks good!


Review powered by cora-code · BYOK · MIT

@ajianaz
ajianaz merged commit 46ae166 into main Oct 15, 2026
17 checks passed
@ajianaz
ajianaz deleted the chore/release-v0.21.1-main branch October 15, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants