Changelog¶
Public release history for semvec on PyPI from 0.3.7 onwards. Earlier releases were development iterations and are not part of the public history.
This changelog highlights what changes for users of the library — added APIs, behaviour changes, fixes that affect existing call sites. Internal refactors, audit-cycle iterations, and test-suite-only changes are intentionally omitted; for the full engineering history, see the project's git log.
The format follows Keep a Changelog; versions follow Semantic Versioning and PEP 440.
[Unreleased]¶
[0.8.9] - 2026-09-17¶
Added¶
-
First-class
postgressession backend.SEMVEC_SESSION_BACKEND=postgresmakes the SQL store authoritative — Postgres is the system of record, so any replica serves any session with noSEMVEC_STATE_PERSISTflag and no sticky routing, the same stateless model asredisandmongo. It reuses the existing SQL store and writes each turn's state and its compliance events in one transaction, and/v1/readyzgains a real reachability probe for it. Install withpip install "semvec[api]"and setDATABASE_URL. The durable Cortex topology and cross-pod Cortex consensus follow the backend too (the next two entries). -
Durable, cross-pod Cortex topology on every durable backend. The whole aggregation layer — cluster records (membership, name, aggregation mode, coupling factor, owner, drift-exempt pin), region records (member clusters, name, consensus threshold, vote window, owner, meta-session id), and observer records (owner, sample interval, meta-session id, registered regions) — now survives a restart and is shared across pods on
postgres,mongo, andmemory+persist, not justredis. Previously the cluster/regional/observer managers rebuilt these empty on every start (only the backing session vector persisted), so a restarted or second pod lost the Cortex topology. No configuration change: the records follow whichever session backend you run. Consequences: adrift_exempt=Truecluster stays pinned across a restart onmongo/postgres; a region's drift-consensus routing is re-established after a restart; and an observer is one-per-tenant across a stateless fleet (two pods no longer create duplicate observers for the same subject). -
Cross-pod Cortex consensus on
postgresandmongo. The network-wide α=0.1 Cortex consensus vector — the shared "global state" every agent's embedding is blended toward — is now shared and authoritative across pods onpostgresandmongo, not justredis. Previously a statelesspostgres/mongofleet blended each agent toward its own per-process observer copy, so there was no network-wide consensus across pods; now every authoritative backend folds into one CAS-revisioned shared vector. No configuration change — it follows your session backend.memoryandmemory+persist (single-pod) keep the per-pod observer, unchanged. This was the last redis-only behaviour in the Cortex layer. -
Opt-in store-side session expiry on every durable backend. Idle sessions can now expire from the store itself on
mongo(SEMVEC_MONGO_SESSION_TTL_S— a TTL index overupdated_at) and on the SQL-backed configs (SEMVEC_SQL_SESSION_TTL_S— an age-sweep in the session sweeper), alongside the existingSEMVEC_REDIS_SESSION_TTL_S. All default to0= never, so nothing changes unless you opt in. Left at the default,mongo/postgressession tables grow until an explicitDELETE /v1/session/{id}or a GDPR erasure — set the TTL if you want automatic cleanup.
Changed — please read before upgrading¶
-
The Community rate limit now holds over HTTP, and a REST turn costs a token. A turn reached no metered function in the core: retrieval draws nothing, and the short-circuit and drift decisions were computed in Python. A Community deployment therefore served turns at measured 51 QPS and refused none of them, while the same work in-process was refused after 51 calls. Both decisions now come from one call into the Rust core,
SemvecState.evaluate_probe, which draws one token per turn. If you drivePOST /v1/runfaster than your tier allows, you will now receive429where you previously did not. Community is 5 QPS sustained on a 50-token burst, unchanged as documented. -
A second bucket, keyed by the licence subject, is shared across the whole process. The existing bucket lives on a
SemvecState, so over HTTP a caller could send no session id, get a new state with a full bucket, and pay nothing. Opening more sessions no longer buys more throughput. Every keyless caller shares the one anonymous subject, so a Community deployment is capped at 5 QPS in total rather than 5 QPS per session; a Pro or Enterprise key carries its own subject and its own budget. This is not a cross-process limit — see licensing for what to use across replicas. -
429responses now carry a realRetry-After. The handler read an attribute the core never set, so it omitted the header it documents. The core now reports the delay it computed and the header is present. -
SEMVEC_STATE_PERSISTis deprecated (to be removed in semvec 1.0). Thememory-backend write-behind SQL gate still works and nothing breaks — but it is superseded by the authoritativepostgresbackend above, which is the same SQL store without the flag or the single-writer/sticky-routing requirement. Selectingmemory+SEMVEC_STATE_PERSIST=1now logs aDeprecationWarningat startup pointing atSEMVEC_SESSION_BACKEND=postgres. To migrate, switch the backend; your Postgres connection string is unchanged. -
SEMVEC_COMPLIANCE_DBis deprecated (to be removed in semvec 1.0) — 0.8.8's hard removal is withdrawn. 0.8.8 removed the variable and refused to start when it was set. 0.8.9 accepts it again: a set variable logs aDeprecationWarningand still wins, so a trail that was written to a separate file keeps being read from there. If you removed the variable for 0.8.8, nothing changes for you.
Every durable backend
now keeps its own audit log — postgres and memory + SEMVEC_STATE_PERSIST in SQL, redis
in Redis, mongo in MongoDB — so the Compliance Pack no longer needs a database of its own.
Setting SEMVEC_COMPLIANCE_DB still works and still wins over the backend's store (an existing
trail is never orphaned), but it now logs a DeprecationWarning at startup. To migrate, run the
Compliance Pack on a durable session backend and unset it — the audit log then lives with the
session state. The only setup that still needs it is compliance on the ephemeral memory
backend.
-
MONGODB_URIis now the mongo connection variable;SEMVEC_MONGO_URIis deprecated. Themongobackend reads the ecosystem-standardMONGODB_URI, matching theDATABASE_URLandREDIS_URLnames the SQL and Redis backends already use — so the connection surface is consistent.SEMVEC_MONGO_URIstill works, and still takes precedence when both are set so no deployment silently switches stores on upgrade, but it logs aDeprecationWarningand will be removed in semvec 1.0. Rename it toMONGODB_URI. -
Fleet-wide rate limiting now works for any backend that configures Redis. The global cross-pod rate limiter previously fired only for the
redissession backend; it now also fires whenREDIS_URLis explicitly set. So a statelesspostgres/mongofleet that points at a Redis gets a real cross-pod QPS/burst limit instead of the previous per-pod behaviour (where N pods allowed ~N× the ceiling). No Redis configured → the per-process limiter is unchanged. Note if you already setREDIS_URLon a non-redis deployment for another reason, your global limit now applies to the compliance//v1surface — intended, but worth knowing before upgrading. -
⚠️ A REST request without a token is served on the Community tier — no licence key needed. The library has always served a
SemvecStatewithout a key as Community; the REST server answered401unless the wheel carried a dev-only build feature, so the same tier was free in-process and key-gated over HTTP. Nowsemvec serveneeds no key: a request with no token runs as the shared anonymous tenant under the Community bucket (5 QPS sustained / 50 burst per session,429beyond it). Pro and Enterprise stay behind the JWT; a token that is present but invalid is still401and an expired one402— a broken key is never silently downgraded. The OpenAI-compatible proxy likewise starts as Community without a deployment key instead of refusing to start.
Read this if your server is reachable by strangers. Until now a missing key kept them out
with a 401. It no longer does: anyone who can reach the server gets the Community tier. They
cannot reach a keyed tenant's sessions — ownership is deny-by-default and the anonymous tenant
is its own tenant — but the licence gate was never meant as your perimeter. If the server
should not serve strangers at all, put your own authentication or network policy in front of
it before upgrading. Note also that the Community bucket is per session: a caller who keeps
opening sessions is bounded by SEMVEC_MAX_SESSIONS and, with REDIS_URL set, by the fleet
limiter on the shared anonymous subject — not by the per-session 5 QPS.
Fixed¶
-
A compliance query no longer fails because one event came from an older embedding model.
SqliteEventStoreranked with numpy and without truncating, so a single stored event of a different dimension made the wholequery_by_userraise and the user's audit trail became unreadable. It now uses the same ranking as the SQL, Redis and Mongo stores: a shorter vector is compared on its shared prefix, and an event whose embedding has no magnitude sorts last instead of being dropped from the trail. -
The Cortex aggregation on the stateless backends follows the observer's strategy. The server-side blend reproduced the coherence gate and the influence weighting in Python and then hard-coded
WeightedAverageAggregation, while the observer uses whichever strategy it was constructed with. Deployments see no change today, because the default is the weighted average; the two are now one implementation and cannot diverge.SemvecCortexObservergainsaggregate_proxies, which returns the aggregate without folding it into its own state. -
A pod whose durable store is unreachable at startup now comes up and reports not-ready, instead of failing to start. Since the Cortex topology became durable, the server rebuilt its region and observer view from the store while constructing the app — and on
redisthat first read includes the persistence check, so a Redis that was briefly down kept the pod from starting at all. Construction no longer depends on the store: the rebuild is logged and performed on the first successful store access instead, and/v1/readyzis where a missing store surfaces, as before. -
A transient consensus-store error no longer fails the turn. With cross-pod consensus on
postgres/mongo/redis, a store that is briefly unreachable during the consensus read now degrades that turn to the per-pod observer (logged), the same way the write path already tolerated it. -
drift_exempt=Truesurvives a restart onpostgres. The pin was written and read only formemory+SEMVEC_STATE_PERSIST, so on the authoritativepostgresbackend it was silently dropped on restart. Both gates now cover both SQL-backed configurations.
Removed¶
- Inert compliance config —
semvec.compliance.ComplianceConfigand its environment variables.ComplianceConfigand the seven variables it parsed —SEMVEC_ENABLE_EVENT_STORE,SEMVEC_ENABLE_RETENTION_SWEEPER,SEMVEC_ENABLE_HMAC_SIGNING,SEMVEC_ENABLE_RS256_JWT,SEMVEC_ENABLE_NUMERIC_EXTRACTOR,SEMVEC_RETENTION_DAYS_CHAT,SEMVEC_RETENTION_DAYS_AUDIT— were parsed by afrom_env()that nothing in the runtime called, and no code read the resulting config, so setting any of them changed no behaviour (and their documented defaults had drifted from the code). They are removed. The Compliance Pack is enabled bySEMVEC_COMPLIANCE(REST) or by constructing its pieces in-process; certificate signing is gated by the presence of the signing key (SEMVEC_COMPLIANCE_PRIVKEY_FILE/_PEM). If you importedComplianceConfig, drop the import — it configured nothing.
[0.8.8] - 2026-08-19¶
Documented in 0.8.9: the three entries marked [documented in 0.8.9] describe behaviour that shipped in 0.8.8 and was missing from this changelog at release time.
Added¶
-
[documented in 0.8.9] Multi-tenant scope, expressed one way:
OwnerScope. Ownership across the whole API — every session, cluster, region and observer method, and every session backend'ssave/load/delete— is now expressed with a single value type,semvec.api.owner_scope.OwnerScope(tenant, agent=None), instead of a bareowner_subjectstring. A tenant-only scope encodes to the same owner string as before, so single-tenant deployments are byte-identical; a two-level(tenant, agent)scope is a new, opt-in shape. Isolation is now also enforced at the storage layer: all four backends (memory, postgres, redis, mongo) filter reads by owner, refuse a cross-tenant write with a scoped compare-and-set, and may adopt an unowned row but never overwrite another tenant's — defence in depth behind the manager's existing ownership check. -
Live dataflow debugger (opt-in). Set
SEMVEC_DEBUG_TRACE=1andSEMVEC_DEBUG_UI=1to open a/debug/page that shows semvec's components and real per-turn dataflow in real time — including cortex state flowing upstream (session → cluster → region → global) and the consensus blended back down. Both flags are off by default and the debug endpoints do not exist unless enabled; a scenario client (scripts/debug_scenarios.py) can generate traffic to watch. -
The redis backend now holds the Compliance Pack's audit trail too. With
SEMVEC_SESSION_BACKEND=redisand the pack enabled, a turn writes the session state and its audit row in a single atomic operation: a write that loses a compare-and-set stores neither, so the trail never records a turn that was rolled back. No separate compliance database is needed.
Nothing changes for a redis deployment that does not use the pack.
Note for Redis Cluster: the script builds its keys internally, so a clustered deployment needs hash tags to keep a session's state and its user's log in one slot. Single-node and Sentinel are unaffected.
user_idon/v1/runand/v1/store— the data subject a turn belongs to. Optional. Supply it with the Compliance Pack on and the turn is written to the audit trail inside the same transaction as the session state, so a compare-and-set that loses discards both and the trail never records a turn that was rolled back. Omit it and nothing is audited.
It is not the licence subject. The licence subject is the tenant, enforced on every
compliance route. user_id is the person, and it is the key
DELETE /v1/compliance/users/{user_id}/memory erases on. Passing the tenant here would put
every end user in one bucket, so one person's erasure request would delete everybody's
memories — which is why there is no default and no fallback.
A turn on a session with no owner is not audited and logs a warning: the compliance routes serve only rows matching the caller's tenant, so an unowned row would be unreadable by anyone.
- MongoDB backend adapter.
/v1/readyznow probes the mongo server, and the turn path writes a turn and its audit rows through one adapter — one CAS, one transaction. - Blended-session honesty and repair on
/forget. Erasing a session whose event log names co-subjects is reported (shared_sessions_erased) and their state is rebuilt from their surviving audit events rather than silently lost.
Fixed¶
.env.exampleis current again. It named 37 variables while the code read 64, so copying it left you unable to configure a session backend, MongoDB, persistence, the audit trail or the debugger. All of them are in, and a test keeps the file in step with the source.DATABASE_URLno longer suggests a default that was deliberately removed, andSEMVEC_TRUSTED_PROXIESis marked not wired — nothing reads it, andX-Forwarded-Foris trusted unconditionally, so filter that header at your proxy.- The live debugger is documented.
/v1/debug/{ticket,stream,recent,topology}, the two flags that switch it on, and the single-use ticket/streamneeds becauseEventSourcecannot send anAuthorizationheader. - The published OpenAPI spec was four endpoints behind the code and is generated and checked
now, with unique
operationIds so client generators can consume it. - Six previously undocumented environment variables are in the CLI reference, including the redis durability escape hatch.
- Tenant isolation at the edges. A license subject containing
/answers a clean 400 instead of a 500; two-level(tenant, agent)scopes work end to end; unowned legacy cluster and region records are denied for every tenant; the debug stream and/recentdeliver only the caller's own events;/forgetalso evicts the live in-process session cache; retrieval on a MongoDB without the Search Index service falls back to in-process ranking instead of failing every turn; event-replay recovery is filtered to one session of one tenant. - Erasure completeness.
/forgetrefuses the certificate (retryable 503) while any state row survives; the event wipe deletes exactly the derivation snapshot, so a turn committed in the race window stays findable;DELETE /users/{uid}/memoryerases state and materialized data too; deleting a session erases its materialized item docs. - Search-Index capability is remembered per deployment. A process that connects to more than one MongoDB no longer carries the first server's "no search service" verdict over to the others, which used to leave server-side retrieval permanently off on an Atlas cluster that supports it. A transient probe failure is still never cached.
- A persist trace event keeps its tenant when the session is evicted mid-flush. The owner now travels with the snapshot instead of being looked up after the store write, so a flush racing the idle sweeper no longer produces an unattributed event that every authenticated debugger can see.
Changed — please read before upgrading¶
- [documented in 0.8.9] ⚠️ Ownership is now deny-by-default — the
None-permissive escape hatch is closed. Previously aNoneowner permitted access: an unauthenticated caller could reach an owned session, and an authenticated caller could reach an unowned/legacy row (a stranger could getGET /v1/metrics200,POST /v1/store200,DELETE /v1/session200 on exactly such a row). Now, across the session and cluster/region/observer ownership checks, access is granted only when the record carries a non-empty owner equal to the caller's:- Anonymous is an explicit, restricted tenant — an unscoped call resolves to the anonymous subject, so it reaches only the sessions/clusters/regions/observers it created, never a real tenant's and never an unowned row.
- Unowned/legacy rows are denied for everyone, rather than handed to whoever asks.
Single-tenant, dev-anonymous and in-process library deployments are unaffected in the common
case (the anonymous tenant still reaches its own sessions). But any code that relied on
cross-reading an unowned session, or on an unauthenticated caller reaching an owned one, is now
refused. POST /v1/compliance/users/{user_id}/forget is likewise owner-scoped per tenant.
-
[documented in 0.8.9] BREAKING for anyone shipping a
semvec.backendsadapter: the store/adapter write API takesscope: OwnerScope, notowner_subject.StateStoreProtocol.save/.load/.load_full/.deleteandBackendAdapter.commit_turnretired the bareowner_subject: str | Noneparameter in favour ofscope: OwnerScope | None. Only third-party backend packages are affected — update your adapter's signatures (see Session backends). Nothing changes formemory,redis,mongo, or for callers of the REST API. -
Server-side retrieval honours the retrieval knobs now — and BM25 off finally means off. A configured cross-encoder and MMR diversity run on the mongo backend too, through the same two rules the in-process pipeline uses. The catch for existing
SEMVEC_MONGO_ITEM_SYNC=1deployments: the lexical$searchstage used to run whether or not BM25 was enabled, and now runs only when it is. SetSEMVEC_HYBRID_BM25=1to keep the previous ranking. -
BREAKING: an audited turn on a standalone
mongodnow fails loudly — MongoDB offers the required multi-document transaction only on a replica set. The error names the one-member fix (mongod --replSet rs0, thenrs.initiate()); turns without compliance events keep working on a standalone. - BREAKING:
SEMVEC_COMPLIANCE_DBis removed. The audit trail now lives in whichever storeSEMVEC_SESSION_BACKENDselects, so a turn and its audit row are written together. A separate database could not share that transaction.
Setting the variable now refuses to start rather than being ignored: switching your trail silently would leave the pack answering from an empty log while your existing events sat in the old file.
Migrating: export the events from the file it names first — nothing migrates
automatically. SqliteEventStore is still available as a library store for reading them. Then
unset the variable and make sure your session backend keeps a log (SQL persistence, redis or
mongo; the plain in-process memory backend does not).
- BREAKING for anyone shipping a
semvec.backendsadapter:BackendAdapterrequires acheck_health()method. It returnsTruewhen the backend can be reached right now, andFalsewhen it cannot. An installed adapter without it is refused at load time with a message naming the member, rather than being accepted and failing later.
Who is affected. Only third-party backend packages. Nothing changes for memory, redis
or mongo, and no configuration changes.
Why. /v1/readyz had no way to ask an installed backend whether its store was reachable.
Its Redis check is name-equality on "redis" and its database check only fires when SQL holds
the session state, so a durable third-party store that was down answered 200 while semvec
reported persistence as enabled — traffic kept arriving at a process whose next write would
fail. /v1/readyz now reports a fourth check, backend, and returns 503 when it fails.
Implementing it. Something cheap that the store answers without touching session data — a
PING, a SELECT 1. Catch your client's exceptions and return False; do not raise. Declare
it as a method, not an attribute: check_health = True is now refused explicitly, because
a health signal that cannot fail reads exactly like a healthy one.
- The Compliance Pack now requires an Enterprise licence. A Pro licence receives
403from every/v1/compliance/*endpoint.
The compliance event log is held by the backend you select — in the same database as your session state, with no separate compliance database of its own. Entitlement to the pack is therefore entitlement to a capability of that backend, and it is sold at Enterprise.
An earlier draft of this entry said the events are "written in the same transaction" as the
state. That was withdrawn while no live path passed its events through, and it now holds: see
user_id on /v1/run and /v1/store below.
If you use the pack on a Pro licence, you need an Enterprise licence. Nothing else changes.
[0.8.7] - 2026-08-03¶
No functional change. 0.8.7 behaves identically to 0.8.6; upgrading is not necessary.
The release contains only corrections to the internal verification and CI layer — normally
out of scope for this changelog per the policy above, listed here because one of those
corrections concerns a check whose silent failure was security-relevant: the guard that is
supposed to prove the Compliance Pack is unreachable without SEMVEC_COMPLIANCE could no
longer fail under FastAPI 0.141. The protection itself — the pack is not mounted without the
environment variable — was and is effective; it simply stopped being verified.
[0.8.6] - 2026-08-03¶
Added¶
-
semvecctl— a management CLI for a running server. A dependency-free HTTP client for the REST API:semvecctl status,semvecctl metrics(Prometheus, HTTP Basic) andsemvecctl memory list|facts|rm|wipe|forget <user>to browse and erase a user's memory through the Compliance Pack. Destructive commands ask for confirmation (or--yes); every command supports--json. Authentication via--tokenorSEMVEC_LICENSE_KEY. -
The Compliance Pack can be mounted (
SEMVEC_COMPLIANCE=1) and then requiresSEMVEC_COMPLIANCE_DB— without a path the server does not start. An event log that is empty after every restart fails the one promise it exists to keep. The pack requires a pro or enterprise licence. -
MongoDB as a session backend (
SEMVEC_SESSION_BACKEND=mongo). Sessions survive a process restart and are visible to every instance behind a load balancer. Install withpip install 'semvec[mongo]', then setSEMVEC_MONGO_URI(no default — a guessed host would look like it worked while losing state) and optionallySEMVEC_MONGO_DB. Unlike the redis backend there is no second store: MongoDB is itself the system of record. If the driver is missing while the switch is set, the server does not start and names the extra; a mistyped backend name still falls back tomemorysilently.
Fixed¶
- The Compliance Pack separates tenants. Its endpoints previously took only the
user_idfrom the URL: anyone holding any valid licence could read and erase every user's memories. This was reachable only because the pack was never mounted anywhere — with the mount added in this version it would have become reachable. Every event now carries its owner, and every route serves and deletes only what belongs to the caller. An erasure certificate names the erasing tenant and counts that tenant's events; it claims no completeness across tenants.
Changed — please read before upgrading¶
ComplianceStaterequiresowner_subjectonce anevent_storeis set. Existing code that attaches the event store without an owner now raises aValueErrorat construction:
# 0.8.5 and earlier — still accepted
ComplianceState(config, event_store=store, user_id="alice")
# 0.8.6 onwards — required
ComplianceState(config, event_store=store, user_id="alice",
owner_subject="<licence subject that owns this data>")
This is intended and will not be made lenient: without an owner you get events that belong to no tenant — and the server serves those to nobody. A lenient default would be exactly the gap closed above. Pass the licence subject under which the data is held.
[0.8.5] - 2026-08-03¶
Fixed¶
-
The mutation feed from
update(..., include_mutations=True)is now usable as a source of truth for a persistence mirror.evictedmeans only "gone from every tier"; a tier change ispromotedordemotedand leaves existence untouched; a consolidation reportsmergedand names the replaced IDs insources. Anyone who was waiting forevictedin order to delete rows was deleting live data and never saw the deletions that were missing. Note:evicteddoes not fire throughupdate()— on that path deletions reach a mirror only asmergedsources. -
Snapshots are roughly a third smaller. The same embedding was written several times when a memory sat in more than one tier or additionally in the context window. Measured at 384 dimensions: 35 % less, both uncompressed and compressed. The fixed portion per snapshot drops from about 164 kB to about 13 kB, which mainly makes frequent write-behind storage cheaper.
-
Restored states are reproducible. Four collections moved on every restore because their order depended on a hash seed. The heaviest was
build_handoff_context(): it named different error patterns after every restore — four restores of the same unchanged state produced four different sets. A restored session therefore told the next one something different, which is the core purpose of multi-session memory. -
Entries derived from code keep their timestamp and access count across a restore. They previously carried the time of loading, which inverted ordering by recency: the most recently parsed material lost its lead.
Changed¶
semvec.compliance.certificates.resolve_priv_pemis now a promised public function (in__all__). It had been reachable without an underscore since 0.8.2, but was promised nowhere and could therefore have been renamed at any time.
Important when upgrading¶
- Snapshot format 9/10. States written by 0.8.5 cannot be read by older versions; those reject them explicitly rather than misreading them. In the other direction, 0.8.5 reads all older snapshots unchanged. Downgrading to 0.8.4 after writing new snapshots is therefore not possible.
[0.8.4] — 2026-08-02¶
No change to the library. This release fixes four defects in the project's verification
tooling: cargo test and cargo clippy skipped the PyO3 bindings directory entirely (305
instead of 310 tests), and the type-checker hook analysed a different checkout than the one
being committed when run inside a git worktree.
The wheels are functionally identical to 0.8.3. Upgrading is not necessary; it is only worth it if you develop on the project itself.
[0.8.3] — 2026-08-02¶
Tenant isolation. If you run semvec for more than one licence subject in one process, upgrade — several endpoints did not check who was asking.
Security¶
-
The
/v1/network/*endpoints now enforce session ownership. All six ignored the caller's identity. Anyone who knew a session id could blend semantic state into a stranger's session, read state back out of it, read another tenant's partition in full — including stored memory text — tamper with another tenant's trust record, and list every session id ever proposed. Present in 0.8.2 and earlier. -
POST /v1/session/createno longer takes over an existing session. Supplying another subject'ssession_idreplaced that session: its memories were gone, ownership passed to the caller, and the original owner then got a 404 on their own session.POST /v1/runwithreset_context: truedid the same. Both now answer 403; re-creating your own id stays idempotent. -
Clusters, regions and the observer stay within one licence subject. A cluster accepted a stranger's session as a member and then both read and wrote it, while that session kept its original owner — so nothing looked wrong from the outside. The same omission existed one level up (a region accepted a foreign cluster, and drift events reached a foreign region) and one level above that (the observer aggregated foreign regions).
-
Partition keys are scoped to the licence subject. One namespace was shared by all subjects, so asking for a
user_idanother tenant held could only be refused — and every refusal revealed that the name belonged to someone else. Sinceuser_idis a caller-chosen string, a wordlist turned that into a directory of other tenants' user names. -
A licence with no
subclaim is now its own tenant. Every such licence mapped to the same anonymous identity, so any two customers holding one were a single tenant — same ownership, same partitions, same trust table, same rate-limit bucket. Each now gets a stable identity derived from the licence itself. Only a caller presenting no licence is anonymous. -
gen-licenserequires--subject. The flag was optional, so the tool could sign a licence without a subject at all.
Operator notes¶
- Partitions created before this release are not reachable under the new keys. They carry no owner, so they cannot be attributed to anyone and are deliberately not migrated. Create them again; the old rows can be deleted to reclaim storage and expose nothing if left.
- Sessions created by a licence without a
subclaim were owned by the anonymous identity and are not reachable under the new derived one. Re-issuing a licence also changes that derived identity, so re-issue before building state on one. regions_sampledin the observer's sample response now counts regions actually read; the number asked for is reported separately asregions_registered. The old field counted deleted regions as sampled.
[0.8.2] — 2026-08-01¶
Deterministic replay and faithful restores — the release to upgrade to if you
persist SemvecState.
Upgrade strongly recommended if you persist SemvecState with tier capacities of
your own. In 0.8.1 the tier capacities were not part of the snapshot, so a state
configured with capacities above the defaults (15/50/200) always came back at those
defaults and everything beyond them was discarded — a freshly written snapshot then
failed its own checksum. to_bytes() → from_bytes() and to_dict() →
from_dict() are affected alike, from the 16th turn at 200-size tiers. Your
existing snapshots are not lost: 0.8.2 raises the capacity of a pre-0.8.2 blob to
hold what it actually contains, rather than dropping memories to a default the
writing state never used. If you use the default capacities, you are not affected.
Fixed¶
TokenCounter.get_summary()no longer computes savings across mismatched turn subsets. Both totals were summed over whichever values were present, so PSS tokens known for five turns divided by baseline tokens known for two produced a valid-looking percentage that measured nothing — 37.5 % where the comparable turns said 75 %.savings_pctnow covers only turns carrying both numbers, andformat_report()computes its absolute "net savings" over the same subset instead of the grand totals. Partial data is ordinary: an interrupted baseline pass, or a turn whose upstream response carried nousageblock, produces exactly this shape.
Behaviour change, reachable through the public SemvecChatProxy.get_summary():
on asymmetric data the reported percentage changes (it becomes correct). On
complete data it is unchanged. Three keys are added — comparable_turns,
comparable_pss_tokens, comparable_baseline_tokens — so a reader can see how
much data the figure rests on; when the basis is partial, format_report() says so
rather than presenting it as complete.
- A malformed SEMVEC_RRF_WEIGHTS is now logged instead of silently ignored. The
fallback to uniform weights is unchanged and correct, but it was indistinguishable
from having configured nothing, so retrieval ran with weights the operator never
chose.
- Snapshots load again at generous tier sizes. If you configured tiers larger
than the defaults (15/50/200),
to_bytes()→from_bytes()raisedchecksum mismatchonce a tier held more entries than its default allowed — from the 16th turn at 200-size tiers. The capacities were not part of the snapshot, so every restore rebuilt at the defaults and dropped the surplus. If you persist state with custom capacities, this release is required. - A restored state now continues exactly where the original left off. Six
pieces of live state were missing from the snapshot, so the next
update()after a restore returned different metrics than the state it was copied from. This affected every database rehydration. - Identical conversations now produce identical state, including above memory capacity where eviction and consolidation are active.
- Evicted memories no longer stay alive internally. They were retained by the clustering layer, so memory use grew with the conversation instead of staying bounded.
MemoryEventhas asequence— a monotonic, per-user position for ordering a replay.event_idis random andcreated_atcannot order two events written in the same clock tick. Existing databases migrate automatically on startup.- The rate-limit error no longer suggests
update_batch(), which does not help: the limiter counts one token per item, so a batch of 50 costs the same quota as 50 calls. - Reranking no longer returns fewer memories than you asked for. If a
cross-encoder produced fewer scores than there were candidates, the extra
candidates were dropped before
top_kwas applied — quietly, with no exception and no log line. Reachable throughmake_cross_encoder_reranker(public since 0.7.5) and the REST rerank path. You now get the full candidate list in input order, plus a warning. - A malformed embedder response no longer hangs the caller. When the backend returned fewer vectors than were requested, the surplus requests were never answered and the caller blocked until its own timeout — reporting a slow embedder when the real problem was a bad response.
- The Rust embedder is found on Windows. With
SEMVEC_USE_RUST_EMBEDDER=1in a source checkout, the lookup missed the.exesuffix and silently used the Python daemon instead. Affects source checkouts only, not installed wheels.
Added¶
update(..., include_mutations=True)tells you what the turn changed in memory —{"kind": "added" | "evicted", "id": ...}. Useful for mirroring memory into your own store; previously an eviction could only be inferred by noticing an absence. Off by default, so existing callers see no change.AsyncEventStorefor async backends (asyncpg, motor). The synchronousEventStoreis unchanged.
Changed¶
- Persisted values are now exact instead of rounded to four decimals. This is
what made restores unfaithful. Metrics returned by
update()are still rounded — those are a report, not stored state. - The type stubs name their keywords.
to_bytes,to_dict,updateand ten others were typed as(*args, **kwargs), soinclude_memory_text,meta=anddedup_threshold=were invisible to editors and type checkers. The stubs are now complete across all 32 core classes, so editor completion and type checking cover the whole compiled surface rather than part of it. - Restore fidelity of the
calculate_*diagnostics is licence-gated. A restore reproduces memories, retrieval and the update trajectory on every tier. The absolute values ofcalculate_fsm(),calculate_metrics()andcalculate_advanced_metrics()are reproduced only on an official wheel, with a Pro or Enterprise licence, under the same licence subject that wrote the snapshot; otherwise they resume from a fresh salt and step at the restore boundary. Expected behaviour, not data loss — see What a restore reproduces. - Note when handling snapshots:
to_dict()andto_bytes()contain your memory text unless you passinclude_memory_text=False. Treat them as user content — encrypt at rest, and do not log them wholesale.
[0.8.1] — 2026-07-31¶
Reliability fixes for the transparent OpenAI proxy's automatic memory
(X-Semvec-Session).
Fixed¶
- Conversation memory now persists through the OpenAI proxy. Requests
carrying an
X-Semvec-Sessionheader failed to save their session state because the proxy-derived session id overflowed a database column, so memory never accumulated across turns. If you run your own Postgres, widen the column once:ALTER TABLE semvec_session_state ALTER COLUMN session_id TYPE VARCHAR(128)(SQLite users need no action). - The proxy no longer stalls on the memory write. Storing a turn into memory could block for 30 seconds and time out on every session-tagged request; the write now runs off the request's event loop, so memory-augmented calls return as fast as plain pass-through ones.
- Internal: the cross-pod Cortex fold now runs inside the compiled core
(
semvec._core.blend_global_vector) instead of an embedded Redis Lua script. The Redis layer reads, hands the vectors to the core, and stores the result under a compare-and-swap — identical results and the same no-lost-update guarantee, so nothing changes for callers.
[0.8.0] — 2026-06-22¶
Scale-out, stateless deployment. A new opt-in Redis session backend lets any replica serve any session, so you can run the API behind a plain round-robin load balancer instead of the sticky routing the in-process backend needs. Default behaviour is unchanged — set one environment variable to opt in; leave it unset and everything works exactly as in 0.7.2.
Added¶
- Stateless Redis session backend (
SEMVEC_SESSION_BACKEND=redis, defaultmemory). With the flag set, Redis holds the authoritative copy of each session and pods become stateless — any worker can serve any session, so the single-writer-per-session / sticky-routing requirement is lifted. Each pod keeps a hot local cache that is version-checked against Redis on every read and writes through an optimistic compare-and-set, so a concurrent write from another pod is rejected rather than overwriting newer data. Postgres remains the durable backing via an off-hot-path write-behind, with automatic recovery if a session is evicted from Redis. Install withpip install "semvec[redis]". See Stateless scale-out. - Faster session serialization. The binary snapshot codec is roughly 5× faster than before on large states. Snapshots written by older builds (JSON, compressed or not) still load transparently, so upgrading needs no migration step.
GET /v1/readyzreadiness probe. A dedicated readiness endpoint, separate from the/v1/healthliveness probe, that reports whether the pod's dependencies are actually serviceable — wire it to your orchestrator's readiness probe so a pod is pulled from rotation during a dependency outage.- Fleet-wide rate limiting. In the Redis backend the configured QPS/burst ceiling is enforced across all replicas rather than per process, so adding workers no longer multiplies the effective limit.
- Deployment artifacts. A production
Dockerfile, Kubernetes manifests underdeploy/k8s/(Deployment, Service, ConfigMap, HPA, example Secret), and a Docker Compose load-test harness underdeploy/compose/.
New environment variables¶
SEMVEC_SESSION_BACKEND—memory(default) orredis.REDIS_URL— authoritative hot session store (redis backend).DATABASE_URL— durable Postgres backing for the write-behind (redis backend).SEMVEC_REDIS_SESSION_TTL_S— idle TTL on Redis session keys (0= no expiry).
[0.7.6] — 2026-07-13¶
Full REST→library parity for the remaining tuning knobs. Additive and backward
compatible: every new parameter defaults to None, which reads the matching
SEMVEC_* environment variable so REST and in-process behave identically;
passing an explicit value overrides the env for that session.
- Added:
SerializerConfig(context_budget_chars=…)— total character budget across all retrieved memory lines (in-process equivalent ofSEMVEC_CONTEXT_BUDGET_CHARS, which previously only affected REST/v1/run). - Added:
SemvecSession.run(rrf_k=…, rrf_weights=…)— RRF fusion tuning for dense+BM25 (equivalents ofSEMVEC_RRF_K/SEMVEC_RRF_WEIGHTS). - Added:
SemvecSession(bm25_rebuild_every=…)/SessionBM25Index(rebuild_every=…)— BM25 rebuild cadence (SEMVEC_BM25_REBUILD_EVERY). - Added:
SemvecSession(auto_extract=…, auto_extract_broad=…, auto_anchor_from_extract=…)— ingest-time literal-cache extraction policy (SEMVEC_AUTO_EXTRACT[_BROAD]/SEMVEC_AUTO_ANCHOR_FROM_EXTRACT). - Docs: added a complete REST env-var → library-parameter map, a recommended
in-process
SemvecSessionquickstart, and documented the previously undocumented parameters. Every onboarding code example is live-tested before release. Committed an OpenAPI 3.1 snapshot (docs/api-reference/openapi.json) and a client-generation section in the REST reference — note there is still no pre-built multi-language client SDK; the Python package is the first-class in-process SDK.
[0.7.5] — 2026-07-12¶
In-process retrieval-quality parity. Additive; existing code unchanged.
- Added:
semvec.make_cross_encoder_reranker(...)— builds the reranker callableSemvecSession.run(reranker=…)expects (wrapping asentence_transformers.CrossEncoder), so in-process callers get the same cross-encoder precision stage the REST path enables viaSEMVEC_RERANK_MODEL, with no environment variable. - Docs: clarified that the low-level
SemvecStateSerializer().serialize()is dense-cosine only, whileSemvecSession.run()adds BM25 hybrid (enable_bm25=True+semvec[hybrid]) and cross-encoder rerank — the stack the documented LOCOMO numbers use. New "Retrieval quality" guidance plus a quickstart warning and the in-process equivalent of the REST BM25/rerank environment variables.
[0.7.4] — 2026-07-11¶
Cross-session Cortex aggregation fix plus a documentation-consistency pass. No API changes.
- Fixed:
use_cortexREST sessions now actually run their cross-session Cortex aggregation. It previously raised internally and fell back to zeroed coherence (cortex_ok=false) on every turn;SemvecCortexObserveraggregation now accepts the server's session proxies.SemvecAgentcallers and the aggregation results are unchanged. - Docs: every REST / Cortex / dedup code example was corrected against the
real schemas and signatures and then verified by running it against the
built wheel; the 0.7.3 additions (
SemvecState(license_key=…),.tier, client-suppliedsession_idon/v1/session/create) are now documented outside the changelog.
[0.7.3] — 2026-06-22¶
Three fixes for issues found integrating against 0.7.2. Existing code keeps
working; license_key and .tier are additive.
- Fixed: sidecar embedder mode (
--embedder-mode sidecar) no longer returns 500 on/v1/storeand/v1/run— the client is now bound to the server's event loop at startup so the threadpool request handlers can embed. - Fixed:
POST /v1/session/createnow honors a client-suppliedsession_idinstead of silently replacing it with a server-generated id (which led to later 404s). - Added:
SemvecState(..., license_key="…")lifts the license tier without exportingSEMVEC_LICENSE_KEYto the environment, andSemvecState.tierreports the active tier ("community"/"pro"/"enterprise"). License verification stays in the Rust core.
[0.7.2] — 2026-06-18¶
Reliability fix: the sidecar embedder client now reconnects automatically after an idle connection is dropped. Previously, when the embedder daemon closed an idle Unix-socket connection — for example a worker left idle through a long client think-time — the next embedding request could hang until its timeout, visible under load as one failed request per virtual user after an idle period. The client now detects the dead connection and reconnects transparently, and retries a request once if its connection drops mid-flight.
Also fixes the ONNX embedder daemon for multilingual models: token_type_ids
is now sent only to models that declare it, so XLM-RoBERTa-style embedders such
as paraphrase-multilingual-mpnet-base-v2 (which expose only input_ids and
attention_mask) run without the Invalid input name: token_type_ids error.
No API or behaviour changes for existing code.
[0.7.1] — 2026-06-16¶
Durable memory, and cross-frontend dedup that survives a restart. Every 0.7.0 call site keeps working — the new behaviour is opt-in.
Added¶
- Durable per-session state persistence (
SEMVEC_STATE_PERSIST=1, default off). With a PostgresDATABASE_URL, each session's semantic state survives a worker restart: state is written-behind (flushed on a periodic tick and on SIGTERM) and lazily reloaded on first access. A graceful shutdown resumes bit-exact; a hard crash loses at most one flush interval. Assumes one owner worker per session (use sticky routing). See State persistence & durability. - Cross-frontend dedup over the Cortex cluster shared session. Every
frontend on a cluster writes into one backing session, so a
dedup_signal({is_update, max_sim, matched_id}) now flags overlaps across frontends. Available onPOST /v1/cluster/{id}/run(with aresponse) andPOST /v1/cluster/{id}/store, and in-process via a sharedSemvecSession(store_qa/run_sync). A per-calldedup_thresholdoverride is accepted on the store path (REST and the in-processstore_qa/update_state, plus*_async). Create a cluster with{"drift_exempt": true}to pin its shared session against regional realignment (a pinned cluster cannot be added to a region). See Cross-frontend dedup. - Optional Postgres sharding (
SEMVEC_STATE_DB_SHARDS, advanced scaling). Comma-separated DSNs spread the state-blob table across backends, keyed bysession_idvia rendezvous (HRW) hashing (adding a shard moves only ~1/Nkeys); metadata tables stay on the primary. - Compressed state blobs. Persisted state and
to_bytes()checkpoints are now compact binary, ~56% smaller than the legacy JSON encoding. Backward compatible:from_bytes()still reads legacy uncompressed blobs, so upgrades need no migration.
Changed¶
matched_idis now durable. Thededup_signal.matched_ididentifier is preserved acrossto_dict/from_dictand a snapshot reload, instead of being regenerated on each load. A frontend can store amatched_idand correlate against it after a restart.
Fixed¶
dedup_thresholdvalidation tightened to the cosine range[-1, 1]on every path that accepts it (update,preview_dedup, the store/run REST endpoints, and the in-process session methods). Out-of-range values are rejected up front rather than silently producing a meaningless decision.
[0.7.0] — 2026-06-04¶
Use Semvec as an in-process library — no server required.
Added¶
SemvecSession— a library facade for one conversation. Construct it with your own embedder and drive the full per-turn loop in-process:
from semvec import SemvecSession, SemvecState, SemvecConfig
session = SemvecSession(SemvecState(config=SemvecConfig(dimension=384)),
my_embedder, SemvecConfig(dimension=384))
result = session.run_sync("How does the deploy pipeline work?")
print(result.context, result.drift_phase)
run() (async) / run_sync() perform the same embed → retrieve →
short-circuit → drift → context-block → update loop the REST /v1/run
endpoint does, returning a TurnResult. Lower-level methods
(store_qa, compute_drift, context_block, triggers/anchors/isolation,
literal-cache, export_state/import_state) are available too. Bring your
own embedder; cross-encoder reranking is injectable.
- Pass SemvecSession(..., enable_bm25=True) to turn on per-session
BM25-hybrid retrieval in-process — no environment variable needed.
- verify_license_token is now importable from the top-level semvec
package for offline license checks; SidecarEmbedderClient and
ComplianceState are now exported from semvec.embedder /
semvec.compliance.
- semvec.cortex.ops exposes dependency-light helpers for multi-agent
coordination (vector coupling, semantic-delta transfer, consensus-trust
EMA) that previously lived only behind the REST cluster/network layers.
See the new In-Process Library guide for a full walkthrough.
Changed¶
/v1/runproduces identical results (the endpoint now shares its implementation withSemvecSession).POST /v1/storenow returns422(not a misleading404) when an existing session's response can't be stored (zero-norm embedding); a missing session is still404.- The cross-session aggregation flag is renamed to
use_cortex. The REST API still accepts the legacyuse_meta_pssalias, so existing clients keep working unchanged.
Fixed¶
- Robustness / observability hardening. A triaged audit replaced
previously-silent failure paths with explicit logging and correct error
propagation across the REST, session, Cortex, compliance and embedder
layers. For callers this means: failures surface (as logs, a
422, or a clean exception) instead of an anonymous500, a misleading404, or a silently-dropped result.TurnResult/RunResponsegain aretrieval_errorflag so a memory-retrieval fault is distinguishable from an empty context, and the sidecar embedder no longer hangs callers when its worker faults.
[0.6.8] — 2026-06-02¶
Maintenance release — the remaining hardening items from the 0.6.7 audit. No API or behaviour changes for typical callers.
Fixed¶
- Python 3.12 fix in the sidecar embedder client. A loop-acquisition
path used the deprecated
asyncio.get_event_loop(), which raises on 3.12 when no loop is running; it now usesget_running_loop()with a fallback. - License-cache scaling. Under multi-tenant load, one token expiring no longer wipes every tenant's cached verification — only the expired token is evicted, avoiding a burst of redundant signature re-verifies.
- Malformed update results raise a clean error. A missing key in an
update result now surfaces as a
KeyErrorinstead of an interpreter-level panic crossing the Python/Rust boundary.
Changed¶
get_client_ipalways returns a string ("unknown"when the peer is unavailable) rather thanNone.
[0.6.7] — 2026-06-02¶
Hardening release. No breaking changes — every public API and return value
is behaviour-compatible with 0.6.6 (verified by a release-readiness audit;
the full Rust and Python test suites pass). The release closes a class of
latent panics that could abort the host interpreter, tightens the REST
surface, and adds input validation that surfaces as 422 instead of
silent acceptance.
Security¶
- Metrics Basic Auth is now constant-time. Credential comparison uses
secrets.compare_digeston both fields, closing a timing side-channel. - Input validation on
POST /v1/session/create.dimensionis bounded to1–16384(previously unbounded — an oversized value could exhaust memory); out-of-range values now return422. session_idrequest fields are constrained to^[A-Za-z0-9_-]+$(max 128 chars), rejecting control characters that could be injected into log lines. The pattern matches the UUIDs the server already mints, so no legitimate id is affected.- New
SEMVEC_TRUSTED_PROXIESenv var (comma-separated IPs/CIDRs):X-Forwarded-For/X-Real-IPare honoured only when the direct peer is a configured proxy. Forward-looking — the client-IP helper is not yet consumed by request handling.
Fixed¶
- FFI-boundary panics removed. Several
unwrap()/expect()paths reachable from the Python bindings could abort the interpreter underpanic = "abort"; they now raise Python exceptions or degrade gracefully (topic-switch detection, pattern matching, tier selection, and the dev-key / getrandom paths). - No more silently-swallowed errors on
/v1/runandimport_state— retrieval and state-deserialization failures are now logged instead of returning an empty/false result with no diagnostic trail. cargo run --bin gen-dev-keysworks again — the binary source had been truncated, which also brokecargo checkwithout--lib.
Changed¶
POST /v1/dedup-checkis now async internally (it awaits the embedder), so a slow sidecar embedder no longer ties up a worker thread under load. The HTTP request and response shapes are unchanged.
[0.6.6] — 2026-05-20¶
Performance release. Closes a real-world pathology in long-term
consolidation: at the heuristic k = n/3, the long-term
tier-consolidation initialiser scaled quadratically in k and
dominated total cost. On a 200 000-record state the consolidation step
took ~52 minutes; this release drops it into the single-digit-minute
range.
No behaviour change for any API or return value — the math of the initialiser is bit-identical to 0.6.5.
Performance¶
-
Tier-consolidation init: previously O(n²) in
k; now O(n) per pick via a persistent min-distance cache. Instead of rescanning every prior centroid on every new pick, the algorithm keeps a persistent min-distance cache and updates it in place after each pick. Measured speedup (default workloadn=10000, k=3333):workload 0.6.5 0.6.6 speedup n=2000, k=666 7.4 s 25 ms ~295× n=10000, k=3333 >17 min/run 633 ms >1500×
Mathematically identical (verified by a regression test that pins the exact pick sequence from the 0.6.5 implementation). The next consolidation bottleneck is profiled in a follow-up release.
[0.6.5] — 2026-05-20¶
Setup-quality release. Closes a foot-gun in the Claude Code / Cursor
MCP integration: bare pip install "semvec[coding]" did not pull in
sentence-transformers, so the MCP server raised
RuntimeError: sentence-transformers is required the first time it
tried to embed anything. The [coding] extra now bundles both
packages, so the MCP server is runnable in one install step.
No behaviour change for any existing API or SemvecState call site.
Fixed¶
pip install "semvec[coding]"now installs everything needed for the MCP server. Previously the extra declared onlyfastmcp>=2.0;sentence-transformershad to be installed separately. The extra now bundlesfastmcp>=2.0plussentence-transformers>=3.0.- MCP-server
ImportErrormessage now points users atpip install "semvec[coding]"instead of the baresentence-transformersinstall.
Docs¶
uv runlaunch alternative documented for Claude Code and Cursor MCP configs — letsuvresolve the project interpreter on the fly. Cross-platform, no hard-coded venv paths, no escaping of Windows backslashes.claude mcp addCLI shortcut documented as an alternative to hand-editing.claude/settings.json.-
Startup timeout guide for WSL2 / slow filesystems. Documents the two env vars Claude Code honours:
Env var Default Covers MCP_TIMEOUT30 s initial connect + tool calls MCP_CONNECT_TIMEOUT_MS5 s /mcp reconnectBoth must be exported in the parent shell — the
envblock insidemcpServers.semvecdoes not reach Claude Code itself. The/mcp reconnect -32001 (Request Timeout)failure mode is now correctly traced toMCP_CONNECT_TIMEOUT_MS(instead of being presented as an unfixable quirk). - WSL2 performance note: moving the project from/mnt/c/...to a native Linux filesystem (~/dev/...) drops MCP-server startup time below 5 seconds and removes the need for both timeout overrides. - Troubleshooting expanded with five new symptoms (ModuleNotFoundError: fastmcp / sentence_transformers,connection timed out after 30000ms,Failed to reconnect ... -32001, silentFailed to connect,2 min startup on WSL2).
[0.6.4] — 2026-05-19¶
A read-only companion to the 0.6.3 dedup_signal: callers can now
ask Semvec before they call update(). Strictly additive — every
0.6.x call site keeps working untouched.
Added¶
-
state.preview_dedup(embedding)— same{is_update, max_sim, matched_id}dict that anupdate()call would attach asdedup_signal, but without storing the candidate or mutating any state. The natural read-only counterpart for RAG / agent frontends that want to decide whether the incoming text is worth feeding into the downstream RAG index:Same per-call
dedup_threshold=override asupdate(). Does not consume the per-state rate-limit bucket — safe for high-frequency polling. Returned dict shape is bit-identical to thededup_signalsub-dict fromupdate(), so a caller's threshold choice transfers cleanly between the two paths. -
REST
POST /v1/dedup-check— HTTP surface for the same computation. Request:{session_id, text, dedup_threshold?}. Response: theDedupSignalpayload that already shipped in 0.6.3. Authenticated like/v1/run. See the DedupSignal user-guide page.
Docs¶
- The "What the signal does not do → No insert suppression" note in
the DedupSignal guide is now followed by the
preview_dedup()story — that is the right tool when you need to decide before storing.
[0.6.3] — 2026-05-19¶
A read-only similarity-hint on every update() call, plus stable
per-memory identifiers. Strictly additive — every 0.6.x call site
keeps working untouched.
Added¶
-
dedup_signalonSemvecState.update()— every update now returns an informational{is_update, max_sim, matched_id}block alongside the existing metrics. The signal lets RAG / agent frontends route an incoming memory as "update of an existing fact" vs. "genuinely new" without spending an LLM call. Storage stays append-only; the caller decides what to do with the hint. See the new DedupSignal user-guide page. -
MemoryUnit.id— everyMemoryUnitcarries a stable UUIDv7 that round-trips throughto_dict()/from_dict(). Used asmatched_idindedup_signal; also useful for any application that needs a stable handle for a memory. Pre-0.6.3 snapshots that lack the field receive a fresh UUID on load (lossless migration). -
SemvecConfig.dedup_update_threshold(default 0.85) — the cosine threshold above whichdedup_signal.is_updateflips toTrue. Tunable globally on the config, or per call: -
REST
/v1/run: response gains an optionaldedup_signalfield with the same{is_update, max_sim, matched_id}shape.nullwhen no state update happened on the call (e.g. a/runwithout aresponsefield).
Docs¶
- New user-guide page: Detecting updates vs. new information (DedupSignal) — use-case-led walkthrough for putting Semvec in front of an existing RAG / agent / ingest pipeline.
[0.6.1] — 2026-05-13¶
Documentation + API hygiene patch on top of 0.6.0. No behaviour change on the happy path; one bucket-exhaust failure mode now returns a clean HTTP 429 instead of a generic 500.
Fixed¶
- REST API:
RateLimitError(raised by the Rust core when the per-SemvecStatetoken bucket is empty) now surfaces as HTTP 429 with a dynamicRetry-Afterheader derived fromexc.retry_after. Previously the exception leaked through as a generic 500, masking the bucket signal from clients that wanted to back off and retry. Pro / Enterprise license tiers bypass the bucket (since 0.3.7), so this change is only observable on Community / unlicensed callers. - Docs:
concepts-glossary.mdreferencedstate.create_resonance_trigger(...)in a copy-pasteable example; the only method that exists onSemvecStateisadd_resonance_trigger(...). Copy-pasted code would have raisedAttributeError. - Docs: Patent-status notice in
enterprise/index.mdpreviously linked to the EPO Patent Register. The application is in the 18-month confidentiality period under Art. 93 EPC and is not yet publicly available — the link has been replaced with an explicit pre-publication disclaimer and a commitment to add the Register link once the application is published. - Docs: Glossary entries for
cluster_fallback_threshold,drift_threshold, resonance-trigger absorption, and anchor/trigger composition no longer leak internal mechanism — interface and behaviour only, per the llms.txt disclosure policy.
Removed¶
- REST API: Unused
slowapi.Limiterscaffold fromsemvec.api.routesandsemvec.api.app(instantiatedLimiter, registeredRateLimitExceededexception handler, setapp.state.limiter). No@limiter.limitdecorator was ever attached to any route, so the scaffold was dead code. Rate limiting has always been enforced one layer down in the Rust core, not in the HTTP middleware stack. Theslowapidependency has been dropped from the[api]extra.
[0.6.0] — 2026-05-13¶
Sharpening release. Adds production-shaped knobs to the REST API
(sidecar embedder, session lifecycle, hybrid-retrieval tuning), keeps
every /v1/run default identical to 0.5.6, and lands measurable
per-turn speed-ups on the hot path.
Added — Retrieval¶
- BM25-hybrid retrieval in
/v1/run. Per-session lexical (BM25) index fused with dense cosine via Reciprocal Rank Fusion before the cross-encoder rerank stage. Default off — opt in viaSEMVEC_HYBRID_BM25=1. Empirical lift on LOCOMO 10-convo (1986 QAs, gpt-4o): +2.6 pp weighted F1 vs the dense-only baseline (0.469 → 0.495). Strongest single-category lift: multi-hop +5.3 pp. Pulls inbm25s+nltkvia the newsemvec[hybrid]extra. - Weighted RRF fusion via
SEMVEC_RRF_WEIGHTS="1.0,0.4"(dense, BM25). Lets you bias the fusion when BM25 hurts single-fact precision on your domain. Unset = uniform 1.0. Companion knobs:SEMVEC_BM25_FETCH_K(default 50),SEMVEC_BM25_REBUILD_EVERY(default 64 ingests between snapshot rebuilds),SEMVEC_RRF_K(default 60). - Cross-encoder rerank stage behind BM25-hybrid. Env-tunable via
SEMVEC_RERANK_MODEL=<hf-id>(e.g.cross-encoder/ms-marco-MiniLM-L-6-v2),SEMVEC_RERANK_FETCH_K(default 50 candidates fed into the cross-encoder),SEMVEC_RERANK_BATCH(default 64),SEMVEC_RERANK_FP16=1,SEMVEC_RERANK_THREADS. Off by default — setSEMVEC_RERANK_MODELto activate. - Tunable retrieval at
/v1/run. Four env knobs replace the previous wheel-baked defaults:SEMVEC_RUN_TOP_K(top-K passed to retrieval, default 5),SEMVEC_MMR_FETCH_K(MMR candidate pool; default 0 = MMR off),SEMVEC_MMR_LAMBDA(relevance-vs-diversity, default 0.5),SEMVEC_CONTEXT_BUDGET_CHARS(total-text budget across all selected memories, default 4 000). The per-memory legacy 150-char cap is gone — sum-of-text is now the constraint.
Added — REST API runtime¶
- Sidecar embedder daemon.
semvec serve --embedder-mode sidecarspawns a single embedder process and points every API worker at it over UDS (default) or TCP. Eliminates the per-worker model load on multi-worker deployments and shares one model copy across--workers N. The Python sidecar is the default. An optional Rust-native sidecar (SEMVEC_USE_RUST_EMBEDDER=1/SEMVEC_EMBEDDER_BIN=<path>) is picked up automatically by the supervisor when the binary is present; see Embedders for the trade-off table. semvec serve --embedder <URL>— point workers at an externally managed embedder daemon (e.g. on a dedicated host). Useful for GPU-pinning the embedder on one node and CPU-scaling the API on others. Coexists with--embedder-mode sidecar.- SessionManager lifecycle. Per-process session table now has an
idle-TTL sweeper and a hard cap.
SEMVEC_MAX_SESSIONS(default 10 000),SEMVEC_SESSION_IDLE_TTL_S(default 1 800 s),SEMVEC_SESSION_SWEEP_S(default 60 s). Eviction is LRU on idle time. In-memory only — sessions evicted by TTL or cap stop existing for that worker; persist via/v1/session/{id}/exportif you need them back. - Graceful SIGTERM drain.
SessionManager.shutdown()is wired to FastAPI's lifespan — on SIGTERM, in-flight requests complete, the embedder client closes cleanly, and the session table empties. Behind a load balancer this enables zero-error rolling restarts. - Embedder LRU cache + in-flight de-duplication. Off by default —
set
SEMVEC_EMBEDDER_CACHE_SIZE=<entries>(10 000 is a good starting point) to wrap the active embedder. Cache hits skip the model entirely; concurrent requests for the same text wait on one in-flight future instead of issuing duplicate model calls. ~2.9× throughput win on repeat-heavy chat traffic; no effect on cold workloads. - Per-request retrieval defaults are read at start-up.
routes.pynow readsSEMVEC_RUN_TOP_K/SEMVEC_MMR_*/SEMVEC_CONTEXT_BUDGET_CHARS/SEMVEC_RERANK_*/SEMVEC_BM25_*/SEMVEC_RRF_*/SEMVEC_HYBRID_BM25once per worker. No per-call kwargs needed — set env, restart, done. See CLI reference for the full table.
Added — Benchmarks¶
benchmarks/run_locomo.py --judge— LLM-as-Judge re-evaluator that reuses the mem0 paper's judge prompt verbatim. Cross-paper numbers become apples-to-apples without a second run.benchmarks/run_locomo_judge.pyremains as the dedicated entry point for re-judging an existing run.- No more
openaiSDK dependency for the judge. The OpenAI-compat adapter is nowrequests-backed, so the[benchmarks]extra alone is enough. Works against any OpenAI-compatible endpoint (vLLM, LiteLLM, OpenRouter, Ollama). find_dotenvfor the judge runner..envlookup walks up from the runner's CWD, so the judge runs cleanly from worktrees and sub-directories — not just the repo root.
Performance¶
All 0.5.6 API surfaces unchanged; numbers below are end-to-end
process-level wins, not micro-benchmarks.
/v1/runasync-native rewrite: parallel embed of query and store-text, ASGI-middleware bypass forDepends(verify_license), LRU-cached Ed25519 verify (256 entries),CORSMiddlewareskipped when no origins are configured, threadpool=200. End-to-end: +63 % cumulative throughput on a mixed/v1/runworkload, +772 % on the QA-only flow vs 0.5.6.- Memory hot-path: single-pass
safe_cosine_similarity(+91 % turn-rate) and additional inner-loop optimisations in the long-term consolidation path (+57 % turn-rate). Storage layout for retrieval matrices reworked to drop conversion overhead. - Prometheus high-cardinality leak fixed in REST request metrics (session IDs no longer leak into label keys).
Removed¶
- All non-LOCOMO benchmark surfaces:
LongBench,MT-Bench,longmemeval, scaling / load / k6 / cortex / consensus / coding runners and their datasets. The Python modulesemvec.benchmarks.longmemevalis gone. LOCOMO is now the single publication-grade bench shipped with the wheel. semvec[longmemeval]extra (folded intosemvec[benchmarks]which now only pullssentence-transformers).openaiPython SDK as a[benchmarks]dependency.
Notes¶
- No behaviour change at defaults. Every new knob ships off; an
unmodified 0.5.6 caller sees the same
/v1/runpipeline. Hybrid, abstain, boosters, sidecar, RRF-weights — all opt-in. pip install "semvec[hybrid]"is required for BM25-hybrid; the base wheel does not ship BM25 dependencies.- LOCOMO drift envelope:
gpt-4ovia OpenRouter is non-deterministic even attemperature=0(~40 % per-QA churn), aggregate drift ≤ ±0.5 pp. See parity envelope for the full picture.
[0.5.6] — 2026-05-05¶
Caller-controlled retrieval-text truncation. Adds the "top-1 ungutted, rest truncated" pattern to the direct-library path and removes the hardcoded 500-char cap from the REST API.
Added¶
SerializerConfig.full_first: bool = Falseinsemvec.token_reduction. When set,SemvecStateSerializerreturns the highest-ranked retrieved memory verbatim and continues to truncate the rest atmax_memory_chars:
from semvec.token_reduction import SemvecStateSerializer, SerializerConfig
cfg = SerializerConfig(top_k=5, max_memory_chars=200, full_first=True)
context = SemvecStateSerializer(cfg).serialize(state, query_text="...")
# Entry 1: full text. Entries 2..5: capped at 200 chars.
max_text_charsquery parameter onGET /v1/state/context(range 1–100 000, default 500). Replaces the previously hardcoded 500-char slice. Combine withfull_first=trueto keep the top hit verbatim regardless of the cap.
Changed¶
- Retrieval-output truncation is no longer wheel-baked on either surface. Defaults match pre-0.5.6 behaviour, so no existing caller needs to change anything.
Compatibility¶
Strictly additive — full_first defaults to False, max_text_chars
defaults to 500. Existing 0.5.x callers see no behaviour change.
[0.5.5] — 2026-05-04¶
Activates the per-tier QPS limits documented in README/PRIVACY since 0.2.0.
Changed¶
- Per-state token bucket is now active as the primary rate-limit layer.
Both
update()and the threecalculate_*methods draw from one bucket perSemvecState. The bucket implements the README-documented tier caps:
| Tier | Sustained | Burst |
|---|---|---|
| Community (no key) | 5 QPS | 50 |
| Pro | 200 QPS | 2000 |
| Enterprise | unlimited | unlimited |
- Sliding-window probe-defence (100/s update, 30/s
calculate_*) is now Community-only. Pro and Enterprise skip this layer because the bucket already covers their use cases at higher caps; the previous "Pro / Enterprise bypass everything" branch admitted unlimited QPS contrary to the documented 200/2000 Pro contract. RateLimitErrormessage now includes the tier, the QPS/burst contract, the retry-after delay in ms, and the upgrade URL.
Compatibility¶
Conversational chat, MCP servers, smoke-tests, and small pytest suites
are unaffected — their typical rates are well below 5 QPS sustained and
fit inside the 50 burst window. Workloads above 5 QPS sustained (heavy
batch ingest, large benchmark replays) should:
- use
update_batch()(one Python call, one bucket-acquire per item but one cross-language hop), - shard across multiple
SemvecStateinstances (each has its own bucket), - or upgrade to Pro / Enterprise.
The compliance event-replay path (EventReplayService) bypasses both
layers — replay must not lock itself out re-folding its own log.
See Licensing guide for the full picture including a per-workload QPS table.
[0.5.4] — 2026-05-03¶
Marketing-wording correction + repo cleanup. Companion to 0.5.3 — no PyPI-wheel content changes.
Changed¶
- "Novelty acknowledged" framing of the EPO Search Report removed in full
from README, the documentation site hero,
llms.txt,llms-full.txt, and the FAQ. The European Search Report is a prior-art search; it is neither a grant nor a standalone novelty determination. Substantive examination is the next step in the EPO process. The patent-pending statement remains, scoped to "application EP 25 188 105 filed at the European Patent Office". - The FAQ "What's the patent situation?" answer now explains the role of the European Search Report explicitly.
Removed (post-tag repo cleanup)¶
- The Cloudflare Worker
semvec-telemetry.versino.workers.devwas deleted along with its KV namespace and any logged records. The endpoint now returns HTTP 404 (Cloudflare error 1042 — no Worker bound to subdomain). - The
telemetry/worker/source tree (the Worker's TypeScript code) was removed from the repository so the public source mirrors the deleted deployment. The Worker source was never part of the PyPI wheel — this is a repo-tree cleanup, not a release change.
No code, no API, no behaviour changes vs. 0.5.3.
[0.5.3] — 2026-05-03¶
Privacy release. semvec no longer phones home.
Removed¶
- Anonymous init telemetry (
semvec._telemetry). The default-on opt-out init ping — version, OS, architecture, Python version, per-machine pseudonym (SHA-256 of a local random salt and the machine ID) — is gone. No HTTP request leaves the package on import. The Cloudflare Worker endpoint is no longer contacted; theSEMVEC_TELEMETRY*environment variables are no longer read; the~/.semvec/telemetry-saltfile is no longer created or used (you can safely delete it from existing installs). The previously cited GDPR Art. 6(1)(f) basis ("legitimate interest in patent enforcement") is withdrawn in full. - HyperLogLog "diversity sketch" (
semvec._diversity). A HyperLogLog cardinality-counting component that posted an estimate to the same Cloudflare Worker at process exit is removed in full. - The atexit-registered ping-completion join that blocked process exit for up to 700 ms waiting for the telemetry socket is gone with the module.
- Custom
User-Agentstring (semvec-telemetry/<version>) is no longer sent because no telemetry request is made.
Why¶
The collection mechanism was disproportionate to its stated purpose,
contradicted itself across three docstrings vs. the runtime default, and
documented an Article 13 transparency gap (the diversity sketch was a
second, undisclosed posting separate from the init ping). Public PyPI
download statistics (pypistats overall semvec) cover the legitimate
install-count signal without any client-side data flow. The Privacy
Notice has been rewritten to reflect the new state — see the
README's Telemetry section on PyPI.
Compatibility¶
- License-JWT verification, inference, state updates, retrieval, REST API, Compliance Pack — all unchanged. License keys are still verified locally against the embedded Ed25519 public key with no network call.
- No public-API surface change. Existing 0.5.x callers run untouched.
- If you set
SEMVEC_TELEMETRY=0in your environment, you can remove the variable; it is no longer read.
[0.5.2] — 2026-05-03¶
Documentation release. No code changes.
Added¶
- Full Claude Code integration guide at
/guides/claude-code/—.claude/settings.jsonwalk-through, automaticSessionStartandPreCompactlifecycle hooks explained,CLAUDE.mdproject rule template, end-to-end example session, troubleshooting. - Coding-Agents overview at
/guides/coding/— decision tree across the four usage paths (MCP + Claude Code hooks, MCP + Cursor rule, in-processCodingEngine, REST API). - Cortex overview at
/guides/cortex/and Cortex over REST API at/guides/cortex-rest/— full coverage of the multi-agent stack including cluster, region, observer, and network endpoints with curl + httpx examples. - Guides landing page at
/guides/and consolidated nav.
Changed¶
docs/guides/compliance.mdopens with an explicit layer table (library vs. cron vs. API) so it is clear which[compliance]capabilities need the[api,compliance]extra.
[0.5.1] — 2026-05-03¶
Documentation release. No code changes.
Added¶
llms.txtandllms-full.txt— machine-readable documentation indexes for AI search engines and LLM crawlers.- Architectural comparisons at
/comparisons/— head-to-head with mem0 (measured on LOCOMO), Letta, and LangChain Memory. - FAQ at
/guides/faq/— when to use semvec vs. mem0/Letta/LangChain Memory, GPU/offline/licensing/patent questions. - JSON-LD
SoftwareApplication+SoftwareSourceCodeschema inindex.htmlfor richer search-engine snippets.
Changed¶
- PyPI metadata: expanded keywords and project URLs (FAQ, Quickstart, Comparisons, REST API reference, PyPI).
[0.5.0] — 2026-05-03¶
First production-stable release. Backward-compatible — every 0.4.x call site keeps working.
Added¶
- Per-call
meta=kwarg onSemvecState.update().state.update(emb, text, meta={"confidence": 0.9, "source": "kg"})lands the per-call dict onMemoryUnit.metaand travels through every snapshot. Symmetrical with the existingComplianceState.update(meta=…)path. include_adaptive_params=Falseprivacy toggle onto_dict()/to_bytes(). Combined with the existinginclude_memory_text=Falseandinclude_literal_cache_text=False, snapshots can now be redacted along all three independent dimensions for hand-off to third-party support.- Ed25519 in
sign_certificate/verify_certificate. Auto-detected from the loaded key — pass an Ed25519 PEM and you get a 64-byte signature; pass an RSA PEM and you keep the existing RSA-PSS-SHA256 path. Cross-algorithm verifies returnFalserather than raising. - Opt-in per-user embedding encryption.
SqliteEventStore(encryption_seed=…)enables AES-GCM with HKDF-SHA256-derived per-user keys. Backup-leak attackers no longer recover raw vectors. Default off (back-compat); the cosine query path keeps working transparently. RetentionSweeperhooks. Newrebuild_worker=,on_before_delete=,on_after_delete=kwargs mirror the FastAPI DELETE/forget enqueue pattern. Hook exceptions are swallowed and audit-logged so a misbehaving observer cannot take down a nightly retention run.- musllinux wheels for Linux (x86_64 + aarch64).
pip install semvecnow works on Alpine / k8s-slim / Lambda-custom-runtime without a compiler toolchain. [jwt]extra forpyjwt>=2.9— covers the issue-side of user JWTs without manual install. Verify-only path ([compliance]) is unchanged.meta_filter=predicate onMultiResolutionMemory.get_relevant_memories. Caller-suppliedCallable[[MemoryUnit], bool]runs after sort, before truncation — Source/Confidence policy can now be applied at retrieval time without post-filtering in Python.protection_score=kwarg oninject_memory— bootstrap a state with persisted long-term decisions / invariants that survive selective forgetting.- Additional internal adaptive-tuning fields on
SemvecConfig. Internal tuning surface; not part of the public configuration contract.
Changed¶
ConsensusEngine.vote_on_proposalrejects unregistered voters. Pre-fix, the engine silently fell back todefault_weightwhen a non-local voter wasn't registered viaregister_instance(…). Now raises typedValueErrorwith a pointer to the registration call. The local-only path (voting_instance=None) is unchanged.
Documentation¶
- New section in
token reduction API: "When does the proxy pay for itself?" — explains the ~10-turn break-even point. Compliance guide: explicit RSA-PSS-SHA256 with MGF1 note alongside the new Ed25519 path;KeyRegistry.register / rotate / revokekeyword-only example.Core API:to_dict/to_bytessignatures expanded with all three privacy toggles plus a "Snapshot redaction" subsection with a worked example.Correcting memories:ResonanceTrigger.weightpicking-table (1.0 default / 2–5 specific / 6–10 hard pin / 0 input-isolation only).
[0.4.5] — 2026-05-02¶
Fixed¶
NegativeAttractorlist now survivesto_dict/from_dict/to_bytes/from_bytesround-trips. 0.4.4 addedstate.add_negative_attractor(...)but the persistence path silently dropped the list — a session snapshot taken after registering attractors restored to an empty list, so a coding agent that built up an "anti-pattern" library across sessions lost it on every restart. Pre-0.4.5 snapshots without the new key restore as an empty list (forward-compatible).
[0.4.4] — 2026-05-02¶
Added¶
- Per-trigger
weight=field onResonanceTrigger(default 1.0, range[0, 10]). A corrected fact's trigger can now outrank topic-default triggers;weight=0silences the boost while leaving the trigger active for input-isolation. The retrieval re-rank computesboost = γ · max_t(strength_t · weight_t). - Anti-resonance in standard retrieval. New
state.add_negative_attractor(error_vector, description, source, severity),clear_negative_attractors(), andnegative_attractor_countgetter. Negative attractors now influencestate.memory.get_relevant_memories(...)directly with a multiplicative penalty(1 − δ · max_strength)against candidates that align with any registered attractor aboveSemvecConfig.negative_attractor_threshold. Previously this was wired only intosemvec.coding.CodingEngine. Default penaltyδis 0.5 (SemvecConfig.negative_attractor_penalty). - Per-call
meta=kwarg onComplianceState.update(emb, text, *, meta=None)— merges intodefault_metawith the per-call value winning on key conflicts. Source/Confidence-tagged events without rebuilding the wrapper. - DELETE / forget endpoints now enqueue a vector rebuild when
set_compliance_dependencies(rebuild_worker=…)is configured. Pure-library callers without a FastAPI session manager keep the previous behaviour. - New guide: Correcting memories — covers the five mechanisms (Recency, Trigger weight, NegativeAttractor, Source/Confidence meta, Hard event delete) with code examples.
Changed¶
- The trigger boost loop no longer breaks on the first keyword match. With per-trigger weights, a heavier-weighted later trigger may produce a larger contribution than a saturated keyword match whose weight is small, so all triggers are now evaluated and the maximum contribution wins.
[0.4.3] — 2026-05-02¶
Fixed¶
semvec.__version__is now a single source of truth. The Python facade had a hard-coded__version__ = "0.4.1"literal that desynced from the actual wheel version when 0.4.2 shipped —pip show semvecreported0.4.2whileimport semvec; semvec.__version__returned"0.4.1". The string is now imported fromsemvec._core.__version__, populated fromCARGO_PKG_VERSIONat compile time. CI guards against the regression.
[0.4.2] — 2026-05-01¶
Fixed¶
- Python 3.10 compatibility. Several modules (
semvec.api.models,semvec.api.middleware.compliance_auth,semvec.compliance.{audit,event_store,extractors,retention}) usedfrom datetime import UTC, which only exists in Python 3.11+. On 3.10, the import raisedImportErroron first use of the API or Compliance Pack. Replaced withtimezone.utceverywhere; behaviour on 3.11+ is unchanged. Affects every 3.10 install of 0.4.0 / 0.4.1.
Documentation¶
- README and the documentation site reframed feature-first; patent appears once at the top of each, the rest reads as product documentation.
- Documentation site published at
https://semvec-docs.pages.dev. - HMAC middleware: documented that the query string is not part of the canonical request — sign the path only, treat query parameters as read-only-shape filters, do not put tamper-relevant input there.
POST /v1/compliance/users/{uid}/forgetoverrides the request-bodyreasonfield with the fixed value"user_request"before the certificate is signed — the cert is an operator-issued attestation, not user-supplied content. Callers that need a different reason useforget_user()from Python directly.
[0.4.1] — 2026-05-01¶
Fixed¶
SqliteEventStore(path=":memory:")now works end-to-end. Pre-fix, every store operation opened a freshsqlite3.connect(":memory:"), soinit_schema()andappend()landed in disjoint ephemeral DBs and the very first append failed withno such table: memory_events. The:memory:branch now keeps a single connection alive for the store's lifetime, guarded by athreading.Lockso concurrent FastAPI workers cannot corrupt the DB. File-backed stores are unchanged./v1/compliance/users/{uid}/forgetreturns a typed 503 when the operator has not configured a compliance signing key. Pre-fix the endpoint deleted the user's events first and then failed with a generic 500 RuntimeError whensign_certificatecould not find the private key — operator-side mis-configuration silently ate user data.forget_user()now resolves the private key before the delete; the endpoint returnsHTTPException(503, detail="compliance_keypair_unconfigured").
Documentation¶
- README and Compliance guide clarify that
[compliance]is the pure-Python extra (cryptography>=42) and that mounting the FastAPI compliance router needs[api]on top (pip install "semvec[api,compliance]"). KeyRegistry.register / rotate / revokedocumented as keyword-only.
[0.4.0] — 2026-05-01¶
Major release: Compliance Pack (semvec.compliance).
Added¶
A new sub-package next to cortex and coding, adding the data-protection and cryptographic-verification layers that regulated tenants need on top of the base SemvecState. Every feature is gated behind a SEMVEC_ENABLE_* env var, all defaulting to off.
Foundations
EntityKindgains three new variants —numeric,date,identifier.ComplianceConfig.from_env()reads five feature flags and two retention day counters.
Event store + replay
- New
MemoryEventschema (UUID, tz-aware UTC, embedding, JSON-safe meta, optional source-event back-reference). EventStoreABC +SqliteEventStore(file-backed, embeddings as JSON arrays). Cosine top-N via NumPy scan.EventReplayServicerebuildsSemvecStatedeterministically from the event log.ComplianceStatewrapper composesSemvecStateand mirrors every successfulupdate()into the store. Failures (dim mismatch, isolation reject) propagate without writing.
Retention + GDPR Art. 17
RetentionSweeper.sweep(retention_days=30)— idempotent purge with audit-log entries.forget_user()— synchronous Art. 17 wipe + signedDeletionCertificate(RSA-PSS-SHA256). Always returns a certificate, even on an empty store.- Embedded public key shipped with the wheel so customers can
verify_certificate(cert)without configuring anything.
Verbatim-precise facts
NumericFact/DateFact/IdFactdataclasses withDecimal, tz-awaredatetime, and ISO-13616 IBAN mod-97 validation respectively. Pure regex — no LLM in the hot path.
HMAC request signing + RS256 user JWT
- AWS-SigV4-style canonical request, HMAC-SHA256, constant-time tag compare.
_internal_verify_user_rs256_jwtfor per-user RS256 JWTs (private key never leaves the client device).KeyRegistryProtocol +InMemoryKeyRegistrywith register / rotate (24h grace) / revoke / lookup.ComplianceHmacMiddlewareenforces the full flow on every/v1/compliance/*request: mandatory headers, ±60 s timestamp window, path-user-id ↔ signed-user-id check, signature verify, nonce-replay check.
REST API
- New router under
/v1/compliance/users/{uid}/...—GET memory,DELETE memory[/event_id],POST forget,GET facts?type=numeric|date|identifier. Forget endpoint serialises the signedDeletionCertificateso callers can verify offline.
Async worker
InMemoryRebuildWorkerdecouples the post-DELETE rebuild from the request path. Single daemon thread,flush()test seam,shutdown()graceful-exit hook.
Dependencies¶
- New runtime dependency:
cryptography>=42(was already in[api]extras for RSA-PSS-SHA256).
[0.3.8] — 2026-04-30¶
Fixed¶
LiteralCache.clear()now wipes every field, not justentities. Pre-fix,clear()leftdecisions,invariants,error_patterns,test_history, andcode_structuresbehind, so a follow-uprecord_*call appended to old data. The new semantics match the method name: a cleared cache is empty.- Bad input to
SemvecState.update()now raises a typedValueErrorinstead of crashing the host process. Two cases covered: dimension mismatch betweeninput_embeddingand the configured state dimension (message tells you both numbers and how to fix); emptyinput_embedding.SemvecConfig(dimension=0)was already handled (raisesConfigurationError) — a regression test pins it.
Documentation¶
- README documents the
SemvecChatProxybreak-even point (~10 turns) explicitly so very-short conversations don't trigger the proxy by default.
[0.3.7] — 2026-04-30¶
Changed¶
- Pro and Enterprise license tiers now bypass the per-state rate limits on
update()andcalculate_*. Paying customers are no longer subject to throttles meant to discourage anonymous probing. Tier is read fromSEMVEC_LICENSE_KEYonce atSemvecStateconstruction and cached. Community / anonymous (no license) keep the existing limits (100/s update, 30/s calculate_*). RateLimitErrormessages now state what to try (slow down, batch viaupdate_batch(), shard across separateSemvecStateinstances, set a Pro/Enterprise license token) and where to upgrade.
Fixed¶
- Privacy toggle now also covers the
LiteralCache. Theinclude_memory_text=Falseargument added in 0.3.6 only redacted the three memory tiers;state.to_dict()was still emitting every literal-cacheentities[].value/context/key, decisions, invariants, error patterns, and code structures in clear. New keyword-only argumentinclude_literal_cache_text=Trueonto_dict()andto_bytes()(default backwards-compatible). Calling both flags at once produces a fully text-redacted snapshot:
snap = state.to_dict(
include_memory_text=False,
include_literal_cache_text=False,
)
Embeddings, kind enum, timestamps, importances, and access counts always ride along — the redacted snapshot is still functionally restorable via SemvecState.from_dict() and retrieval against it works.
Earlier releases (0.3.0a1 through 0.3.6) were development iterations and are not part of the public history. Versions on PyPI before 0.3.7 have been removed.