DEV Community

Cover image for yq-nova-agent v0.4.1 — a regular, periodic maintenance release
YQteam
YQteam

Posted on

yq-nova-agent v0.4.1 — a regular, periodic maintenance release

What is yq-nova-agent?

If you've been around the AI-agent block for a while, you've probably hit the same wall I have: where do I put the agent's memory?

Vector DBs are heavy (Pinecone, Qdrant, Weaviate — all need a service). Mem0 is cloud-only with a non-trivial price tag. SQLite + FTS5 is fine, but then you still have to glue the embedding provider, the graph, the GC and the SDK yourself.

yq-nova-agent is a small Rust core that gives you all of that in one SQLite file:

  • remember / recall / forget over HTTP or Rust SDK
  • Semantic search (pluggable embedding: OpenAI-compatible, Zhipu, Qwen/DashScope, Baichuan, Jina, mock)
  • Hybrid ranking (semantic + FTS5 + graph), fused with RRF
  • Entity-relation graph with BFS traversal
  • Multi-tenant namespaces (x-namespace header + per-namespace API keys)
  • Background GC (TTL + importance-based forgetting, graceful shutdown)
  • One CLI: yq-nova remember | recall | forget | stats | list | tags
  • Ecosystem: TypeScript SDK, Python client, LangChain and LlamaIndex integrations, an MCP server crate

If that sounds like the kind of "just plug it in and go" memory layer you wish you had — read on, because 0.4.1 is one of the most useful patch releases this project has shipped.


The headline changes in 0.4.1

1. Namespace parity for the Python and JavaScript SDKs (#77, #71)

Since v0.4.0, the server has had POST /v1/namespaces and GET /v1/namespaces for managing tenants. But until 0.4.1, the Python and TypeScript SDKs didn't expose those endpoints — so multi-tenant SaaS deployments were left with two options: drop down to raw fetch, or smuggle the request body through a server that doesn't validate names.

0.4.1 fixes that.

Python:

from yq_nova import YqNova

client = YqNova(base_url="http://localhost:7999")

ns = client.namespaces.create(name="acme", description="ACME tenant")
client.remember("user prefers dark mode", namespace="acme", importance=0.8)
recalls = client.recall("user preferences", namespace="acme", top_k=5)
Enter fullscreen mode Exit fullscreen mode

Behavior to watch:

  • create_namespace rejects the reserved default name and reuses the same character rules as with_namespace, so the Python namespace name and the Rust with_namespace argument can never disagree.
  • update_namespace sends only the fields you pass — the server leaves the rest untouched, so partial updates are safe.

JavaScript / TypeScript:

import { YqNova } from "@yq-nova/sdk";

const client = new YqNova({ baseUrl: "http://localhost:7999" });
const ns = await client.namespaces.create({ name: "acme" });
await client.remember({ content: "...", namespace: "acme", importance: 0.8 });
const hits = await client.recall({ query: "...", namespace: "acme", top_k: 5 });
Enter fullscreen mode Exit fullscreen mode

Both SDKs are still 100 % wire-compatible with the existing /v1/namespaces HTTP routes, so a token minted server-side round-trips through either SDK without drama.

2. Embedding-retry no longer panics on bad config (#73)

I have a soft spot for crash-only bugs. This is one of those.

let cfg = RetryConfig {
    max_attempts: 0,   // "I really don't want any retries, just fail fast"
    base_sleep: Duration::from_millis(50),
    max_sleep:  Duration::from_secs(2),
    ..Default::default()
};
// pre-0.4.1: panic. post-0.4.1: 1 attempt, returns Err(EmbeddingError::AttemptsExhausted).
Enter fullscreen mode Exit fullscreen mode

Pre-0.4.1, RetryConfig { max_attempts: 0, .. } panicked on the first retry attempt — which is exactly what you don't want, because the surrounding caller was already written to handle Err, never the panic. Likewise, a base_sleep > max_sleep panicked because the sampling range was logically empty.

0.4.1 clamps:

  • max_attempts = 0 → forced to 1 (you'll still get one attempt before the error bubbles up).
  • base_sleep > max_sleep → both clamped into a valid sampling window.
  • The internal backoff uses saturating_mul, so an extreme base_sleep value can't overflow.

The contract — "you get an Err, never a panic" — was already the documented behavior; 0.4.1 just makes it true.

3. Server now forces the authenticated namespace on recall and list (#70)

If you mint an API key tied to namespace acme and a curious developer writes:

curl -X POST http://127.0.0.1:7999/v1/memory/recall \
  -H "Authorization: Bearer $ACME_TOKEN" \
  -H "x-namespace: internal" \
  -d '{"query":"foo"}'
Enter fullscreen mode Exit fullscreen mode

…v0.4.0 would happily scope the query to internal, because the request body's / header's namespace was treated as authoritative.

0.4.1 makes the token the authoritative namespace for recall and list, full stop. If the request header disagrees, the server pins to the token's namespace. PR #70's name spells it out: force the authenticated namespace on recall and list filters.

This is the kind of PR that closes a CVE-shaped hole without making it a CVE — the existing single-tenant deployments are completely unaffected (they only have one namespace, and the token binds to it anyway).

4. Three SQL/LIKE correctness fixes (#53, #74, #72)

These read as small in the changelog; in production, each one would silently hurt someone.

#53 — empty tags_all filter now matches nothing:

// pre-0.4.1: tag-filter list  → "match everything"  
// post-0.4.1: tag-filter list  → "match nothing", caller fixes the request
{ "tags_all": [], "query": "..." }
Enter fullscreen mode Exit fullscreen mode

The fix is straightforward: an empty tags_all constraint is a hard constraint with zero categories to satisfy, so the SQL layer short-circuits to zero rows. This eliminates a silent over-recall bug where a UI bug accidentally landed an empty filter and the user got everything back.

#74 — LIKE wildcards are now escaped in entity-name prefix filters:

-- pre-0.4.1:
SELECT * FROM entity WHERE name LIKE '%100%off%';
-- Returns every entity whose name contains '100' followed by 'off'.
-- Completely misses an entity literally named '100% off'.

-- post-0.4.1:
SELECT * FROM entity WHERE name LIKE '%100\%off%' ESCAPE '\\';
-- Matches only the literal string '100% off'.
Enter fullscreen mode Exit fullscreen mode

#72 — HTTP provider error bodies truncate on UTF-8 boundaries:

If a remote provider responded with a 4 KB error body containing a 4-byte emoji right at the truncation boundary, you used to get a half-character in your logs (or worse, in a downstream JSON encoder). Now the truncation walks to the nearest character boundary first.

5. CI governance gets tightened (#69, #60, #58, #75, #76)

A healthy project eventually needs to clamp down on its own automation. 0.4.1 does that:

  • #69 scopes each workflow's concurrency group by workflow name, so re-running workflow A can't cancel reviewers for workflow B.
  • #60 teaches @yq-pm-junior not to review its own posts and not to duplicate a post inside the same review thread.
  • #58 wires a small YQ review-team (5 bots + PM gate) into PR review.
  • #76 adds CODEOWNERS requiring a human reviewer for everything under .github/.
  • #75 explicitly steps @yq-pm-junior aside on those same .github/ PRs — those become human-only by design.

If you're a contributor: you'll see less bot noise in your PR threads. If you're a maintainer: you'll see less "did the bot really just approve itself?" drama.


What's intentionally not in 0.4.1

Open PRs that landed on the floor of the 0.4.0 → 0.4.1 window but didn't make the cut, because they were either still under review or landed slightly after the changelog was cut:

  • #81 — fix(js): correct MergeEntitiesOutput and type the remaining SDK responses
  • #80 — feat(langchain, llamaindex): add knowledge-graph tools to both framework integrations
  • #79 — feat(sdk): add graph query methods to EmbeddedNova
  • #78 — feat(js): add namespace management methods to the TypeScript SDK
  • #57, #56, #55, #48, #47, #46, #45, #44, #42, #40, #37, #36, #35, #20 — various open fixes, refactors, perf work, and a feature about scoping MCP tool calls to a namespace

These will roll up into 0.5.0 (or whatever the next bump turns out to be).


How to upgrade

# Docker
docker pull yqteam/yq-nova:0.4.1
docker compose down && docker compose up -d

# crates.io
cargo install yq-nova-server --locked --version 0.4.1

# Source
git checkout v0.4.1
cargo build --release -p yq-nova-server --bin yq-nova
Enter fullscreen mode Exit fullscreen mode

The yq-nova-data volume (SQLite at /data/yq-nova.db) is untouched. There is no data migration step.


Why a periodic maintenance release matters

v0.4.1 is the kind of release the project has been needing for a month, and it shows: the highlights aren't "look at this shiny new feature", they're "this panic is gone, this over-recall is gone, this silent privilege escalation is gone, and our SDKs now agree with the server".

That's the boring stuff that turns a project into a project you can build on.

If you want to follow along, the full release notes live here:
👉 https://github.com/YQteam-hq/yq-nova-agent/releases/tag/v0.4.1

And the changelog is the canonical source of truth:
👉 https://github.com/YQteam-hq/yq-nova-agent/blob/main/CHANGELOG.md

— The YQ team

Top comments (0)