The record

Every release, and the notes that shipped with it.

CortexDB keeps a record of when every fact was learned and how long it held. This page is that same record, kept for the database itself — 74 releases, nothing summarised away.

Find

74 releases

v0.11 series
1 releaseOctober 8, 2026 – October 8, 2026
v0.11.0Breaking

every write through a replicated log, cluster mode (new), and a one-way data upgrade

Breaking: this is a one-way data upgrade. The first start of 0.11.0 migrates the data directory, and no 0.10.x release can open it afterwards. Take a cold backup first and upgrade by hand (below). Do not use the console's "Update now" button for this release, and note that cortexdb/cortexdb:latest now points at 0.11.0: pin cortexdb/cortexdb:v0.10.5 until you upgrade on purpose.

Full notesHide notes21 notes

The API does not change: the same routes, request and response shapes and the same 11 capabilities as 0.10.5, and every client released with 0.10.5 keeps working.

Highlights

  • Every write goes through a replicated log. The event log is now the state of a Raft group, written only by the group's apply. A single server runs one such group with one member: a write is acknowledged once its log entry is fsynced, committed and applied, and every index is built from the log. A single server is still one process, one data directory and zero configuration.
  • Cluster mode (new, opt-in). cortexdb node --config FILE starts a metadata, data or ingress node. A cluster holds shards of 1 to 5 replicas: a shard stays available while a majority of its replicas is up, only its leader serves and reads are linearizable, a lagging or replacement replica catches up by a streamed snapshot, a scope moves between shards on the operator's command while it takes traffic, and a shard that lost its quorum can be restored from one replica's backup. Nodes talk over mutual TLS with pinned keys. The operator places shards, replicas and scopes with cortexdb admin; nothing is placed, split or rebalanced automatically.
  • Embed once per shard. On a cluster, a shard's leader embeds each write once and replicates the vectors through the log, so embedding spend is the same at three replicas as at one.
  • Fixes for defects 0.10.5 still has: a write acknowledged indexed could be lost from the full-text index by a crash within about 100 ms; a server killed during a forget's index commit could fail to start again; the index audit counted every memory twice; a layer build could stall the server behind a scope-registry change; a scope read racing an erasure could fail with SCOPE_INDEX_CORRUPT.
  • Performance. Measured against 0.10.5 on a single server: every write and read latency within 5 % of 0.10.5's at p50 and p99 (or within 0.10.5's own spread between rounds), default-wait write p99 21-28 % lower, a forget by id 24-30 % faster, the same throughput, and a restart at 100,000 events ready 10-17 % sooner. One fixed cost: about 13 MiB more memory at ready on an empty data directory.

Before you upgrade

  • The first start migrates the data directory, one way. It keeps a copy of the event log as it was (pre_stage2/ in the data directory), then moves the server's derived rows, the idempotency records, vocabularies, temporal phrases and retention policies into the replicated log. On a large directory this start takes longer, and needs up to two to three times the event log's size on disk while pre_stage2/ is kept.
  • The one-click updater must not do this upgrade. A 0.10.x server's Updates page offers 0.11.0 like any other release, but its updater does not know the upgrade is one way: if the migration outlasted its health check, it would start 0.10.x again on a directory 0.10.x cannot open. Upgrade by hand, then rebuild the updater from 0.11.0's files for later releases. From 0.11.0 on, the updater recognises one-way releases and asks for explicit consent.
  • The legacy dual WAL is removed, as 0.10.x announced: CORTEX_UNIFIED_WAL=0, or a data directory that still holds v1_wal/, stops the server at start (exit 78, LEGACY_WAL_RETIRED). Run cortexdb migrate-unified-wal --data-dir DIR --confirm first.
  • A storage or fsync error now stops the process (exit 74, WAL_FATAL). Run the server under a supervisor that restarts it (Docker restart: unless-stopped, systemd Restart=always).
  • CORTEX_WAL_SYNC=0 no longer weakens acknowledged writes: every write is fsynced before it is acknowledged.
  • Retired variables: CORTEX_BULK_DEFER_VECTORS (ignored with a warning), CORTEX_DRAIN_EMBED_CONCURRENCY and CORTEX_V1_MIRROR_MAX_IN_FLIGHT (ignored). Any non-empty CORTEX_CLUSTER_* variable on a single server exits 78 (CLUSTER_MODE_UNSUPPORTED): a cluster is configured through cortexdb node.
  • Restoring a cold backup with cold_backup.py needs a 0.11.0 binary (--cortexdb PATH), which checks the archive before it is restored; with Docker, docker cp the binary out of the image.
  • Run a bare binary on jemalloc, as the container image does (LD_PRELOAD naming libjemalloc.so.2 and the image's MALLOC_CONF).

Upgrade from 0.10.5

  1. Pin cortexdb/cortexdb:v0.10.5 so nothing restarts the server on a newer image while you work.
  2. Stop the server with a 120 s grace period (docker stop -t 120 cortexdb).
  3. Take a cold backup of the stopped data directory (a tar of it is enough). It is the clean way back.
  4. If the directory still holds v1_wal/, or you run with CORTEX_UNIFIED_WAL=0, migrate first: cortexdb migrate-unified-wal --data-dir DIR --confirm.
  5. Start cortexdb/cortexdb:v0.11.0 on the same directory with the same environment. Wait for GET /v1/admin/ready to answer 200.
  6. Check GET /v1/admin/version (version 0.11.0, the same 11 capabilities) and read back a few memories you know. Before any write or recall, vectors.embed_inputs on GET /v1/admin/metrics is 0: the upgrade re-embeds nothing.
  7. Once you trust the release, delete pre_stage2/ from the data directory. Until you do, it holds the event log as it was, erased events included, and erasures report that copy as pending.

Coming from 0.10.4 or earlier: read the release notes of the releases in between for what they change for clients, then upgrade straight to 0.11.0 with the steps above.

Going back to 0.10.x

  • From the cold backup (preferred): restore it into an empty directory and start the 0.10.x release you came from. You get the directory exactly as it was before the upgrade.
  • From pre_stage2/: stop 0.11.0, replace unified_wal/ and v1_idempotency/ with the copies in pre_stage2/, delete derived_wal/, shard/ and pre_stage2/, and start 0.10.x. Keep vector/.

Either way, every write made since the upgrade is lost, and every memory forgotten or erased since the upgrade comes back: issue those forgets and erasures again on 0.10.x. Both ways back were tested with 0.10.5, and 0.11.0 upgrades the directory again afterwards.

What API clients will notice

  • Three new retriable 503s on writes: NOT_LEADER (never on a single server once it serves), WAL_UNAVAILABLE (nothing was written) and WAL_OUTCOME_UNKNOWN (the write may have committed: retry with the same idempotency key, which replays it if it did). A client that retries on retriable: true needs no change.
  • A replay returns the original wal_offset (0.10.x answered 0).
  • A failed write keeps its idempotency key: INDEXING_FAILED is retriable: false, and the same key replays the failure, so resend under a new key.
  • A cancelled write answers 409 CANCELLED on a replay or a wait (0.10.x answered 200).
  • GET /v1/experience/status answers 503 WAL_UNAVAILABLE when it cannot read a write's records, instead of found: false.
  • Size limits: a capture record above 4 MiB less 64 KiB is 422 INVALID_ENVELOPE (alone in a bulk); a vocabulary, temporal phrase or retention policy above 4 MiB is 413 WAL_TOO_LARGE.
  • Forgets and erasures commit through the log in entries of up to 4,096 events and answer once the server has applied them. A redact_events forget by subject, entity or predicate now deletes the matched events, and a forget's cutoff is never earlier than the newest event the log already holds.
  • A same-key replay with ?wait=indexed that the indexer has not reached waits within the write's wait budget and can answer 408 WAIT_TIMEOUT.

Cluster mode: limits in this release

  • Scope placement, replica placement and scope moves are operator commands; one shard per data node.
  • A request whose scopes live on different shards is refused (422 CROSS_SHARD_REQUEST); blob routes, by-id reads without ?scope=, cross-shard listings, audit, import, export and most admin routes are not served by the ingress (501 CLUSTER_ROUTE_UNSUPPORTED).
  • Enrichment-derived memories, blob bytes, erasure receipts, the usage ledger and audit rows stay on the node that made them; they are not replicated.
  • A large forget or erasure delays every request on its shard for a few seconds while replicas apply it.
  • With a real embedding provider at three replicas, ?wait=indexed writes have a p99 about 24 % above the same cluster without vector replication, because they wait for the leader's embedding to travel through the log; default-wait writes and reads are not affected.
  • Every node of a shard must run the same release: a replica on an older binary stops rather than apply a newer log entry differently.
  • The public ingress port has no TLS: put a terminator in front of it. Tested within one cloud region across three availability zones; not yet across regions or a WAN.

Clients

No client is re-released with 0.11.0: the versions published with 0.10.5 work against it unchanged.

PackageVersion
Python SDK cortexdbai (PyPI)0.12.0
TypeScript SDK cortexdbai (npm)0.10.1
MCP server cortexdb-mcp (PyPI)0.8.0
CLI cortexdb-cli (PyPI)0.6.0
Connectors cortexdb-connectors (PyPI)0.2.22

Known issues

  • About 13 MiB more memory at ready on an empty data directory than 0.10.5; it does not grow with the data.
  • Going back to 0.10.x loses the writes made since the upgrade and brings back what was forgotten or erased since.
  • Everything listed under "Known issues" in the 0.10.5 release notes still applies.

The full release notes, with every change for single-server operators and the cluster's limits, are bundled with the server (console › Updates, RELEASE_NOTES_0.11.0.md).

v0.10 series
5 releasesSeptember 28, 2026 – October 7, 2026
v0.10.5

fast erasure on long-lived stores, no spurious `wait=indexed` timeouts, cross-customer answers that mean the same in every language, and a Settings page that keeps your keys

0.10.5 is a fix release on top of 0.10.4, the last published release. Everything in RELEASE_NOTES_0.10.4.md still applies; this page lists only what changed since. It adds no column family, directory, WAL row kind, index field, key prefix or on-disk format, and nothing is migrated: a 0.10.4 server opens and serves a data dir 0.10.5 has used (see "Rollback").

Full notesHide notes58 notes

Summary

  • Erasure and its proof are fast again on long-lived stores. On a store whose old retry records had expired, POST /v1/erasures and GET /v1/erasures/{id}/verify each took 25-60 s for one customer, close to a 100 s proxy origin limit for a larger one. Measured on a copy of a store of about 130,000 events, one customer of 219 events: 24.6 s to erase and 24.8 s to prove on 0.10.4, 0.26 s and 0.02 s on 0.10.5. What an erasure deletes and proves is unchanged.
  • ?wait=indexed no longer answers a spurious 408 WAIT_TIMEOUT. A write whose event was already indexed could wait out the whole 30 s budget and answer 408 with waiting_on: "stage". The wait no longer misses the moment the stage completes. The same fix covers wait=consolidated, bulk writes and the lifecycle waits.
  • Cross-customer questions in Hindi or Hinglish no longer answer 422 because the model planner wrote a time phrase the server cannot resolve ("all time"). A phrase that means all time is dropped with a note; any other unreadable phrase falls back to the rules planner, so a question about last month is never silently answered over all data.
  • "Read every conversation" answers agree across languages and runs, and keep the question's meaning. A plain topical question ("which customers discussed a loan or EMI on a call") reads the same way in English and in Hinglish, and a complete read is reused (in memory only) by a later answer over the same data. A negation, a conjunction, a specific verb or a "why" question reaches the readers as asked (for Hindi and Hinglish, as the planner's faithful English translation), never reduced to its topic words. Details under "Read-all answers".
  • Settings: saving a card with its key field empty keeps the stored key, and the "take effect after a restart" banner clears after the restart.
  • Label limits are announced, not enforced. 0.10.5 adds limits on context.labels (count, size, empty labels, control characters) that a later release will enforce by default. They are off unless an operator sets CORTEX_LABEL_LIMITS=on. Check your integrations now; details under "Label limits (announced)".
  • Docs: the scope types each deployment preset allows (the hosted service allows 10), a warning that a forget without cascade keeps the memories and how to delete them, and directives.extract documented as a list.
  • No client release. Every client published with 0.10.4 works against 0.10.5 unchanged.

Upgrade from 0.10.4

Before you upgrade: what clients and operators will notice

Only deployments with the experimental cohort lanes on (CORTEX_COHORT=on, CORTEX_SUBTREE_RECALL=request) see the first four items:

  • cohort.read (an answer that read every conversation) has new fields: reused, computed_at, plan_fingerprint, cohort_fingerprint, topic (the plan's canonical topic) and reader_question (the question the readers got, when it is not the asker's own). A reused answer says so (reused: true, the earlier read's computed_at, a note starting result reused:) and reports no map, theme or check call of its own. The plan echo (cohort.plan.query) carries the canonical topic.
  • A model-planned answer can carry new notes: planner window '<phrase>' ignored: the question is answered over all data; LLM planner unusable (its window '<phrase>' is not a time phrase the server can resolve); a glossary note ending (operator glossary, binding: added to the LLM plan, which had left it out); and the readers were given the question in English as "..." (the planner's translation).
  • A Hindi or Hinglish question on the ranked lane (cohort.read: "sample", or read-all off) also searches with the asker's own words, beside the plan's English query, so its excerpts can differ from 0.10.4's.
  • With CORTEX_COHORT_PLANNER=auto or llm, the planner call asks for one more field (reader_question, about 20 output tokens).
  • The internal Settings route behaves differently (PUT /internal/ui/settings/{id}, used by the bundled console): an API key sent blank or null is kept and listed in a new kept field of the response; only "clear": ["api_key"] removes one, and sending a value and a clear for the same field answers 400 BAD_REQUEST. Connections report pending_fields. The console in the same binary is updated to match. A script that sent "" to remove a key now keeps it silently (the response lists it under kept): send clear instead.
  • If a 0.10.3 console ever saved a card with its key field empty, that key is still missing after the upgrade. That console wrote an override that removes the key at every boot, and 0.10.5 does not rewrite your settings_overrides.json: it logs SETTINGS_OVERRIDE_CLEARS_SECRET naming the entry. Re-enter the key in Settings (or delete the entry from the file) and restart.
  • One new boot log line says whether the label limits are enforced (label limits: not enforced by default).

Nothing else changes for a client: no new route, no new required field, no changed default response.

Upgrade steps

Nothing is migrated and there is nothing to run afterwards. Replace the binary or image and restart as for any release, giving the server 120 s to stop. Then check:

  1. GET /v1/admin/version: crate_version 0.10.5 and the git_sha you deployed. The capability list is the same 11 names as 0.10.4.
  2. GET /v1/admin/ready: ready: true.
  3. Search the boot log for SETTINGS_OVERRIDE_CLEARS_SECRET (see above).
  4. Before a later release enforces the label limits, check your integrations' labels (see "Label limits (announced)"); CORTEX_LABEL_LIMITS=on on a staging server shows what would be refused.

Upgrading from 0.10.3 or earlier

0.10.5 contains everything in 0.10.4. Read RELEASE_NOTES_0.10.4.md, "Upgrade from 0.10.3" (and from there the earlier notes) first; this page applies on top.

Fixes

Erasure and its proof on long-lived stores

An erasure and its proof find the idempotency (retry) records of every event they touch through the idempotency store's event-to-record reverse rows. Those reads were prefix scans without an upper bound. When a store's old retry records expire, the boot clean-up deletes them, leaving one deleted row per expired record, and a lookup whose prefix held no live row walked every one of them before it stopped. On a store with about 130,000 expired records every erase and every proof took 25-60 s. The reads now stop at the end of their prefix, so the same steps take well under a second; a test checks that the bounded reads return exactly what a full scan returns for every prefix shape.

One customer's erasure with proof, same store copy0.10.40.10.5
POST /v1/erasures (execute)24.6 s0.26 s
GET /v1/erasures/{id}/verify24.8 s0.02 s
Proofclean, 13 of 13 storesclean, 13 of 13 stores

?wait=indexed and the lost stage wake-up

The stage wait read the event's stage first and subscribed to stage notifications only then, and its 500 ms poll re-checked only for failures. When the stages finished between that read and the subscription (on an idle server they finish within microseconds of the write), the notification was missed and the request waited out the whole budget, answering 408 WAIT_TIMEOUT with waiting_on: "stage" and waited_ms near 30,000 for an event that was already indexed. The wait now subscribes first, then reads, and reads again on every wake-up and every poll. A failed stage still fails at once, a real timeout still answers 408 at the deadline, and a 408 with waiting_on: "indexer" (a real indexer backlog) is unchanged. Two deterministic tests reproduce the race: both time out on 0.10.4's code and pass on 0.10.5's. The race is rare under load: about 51,600 wait=indexed writes against the release candidate (sequential and concurrent writers, and a write, seed and forget load) gave no 408, and the same loops gave none on 0.10.4 either, so the tests are the proof, not the load runs. The race matches an intermittent 408 in this project's own CI, which has not recurred in the release runs.

Cross-customer questions in Hindi or Hinglish: time windows

With the model planner on (CORTEX_COHORT_PLANNER=auto or llm), a question such as "kin customers ne kabhi bhi ..." could get a plan whose window.natural was a phrase the executor cannot resolve, and the answer was 422 INVALID_BODY naming plan.window.natural. Now, on a model-written plan only:

  • a phrase that means no window at all ("all time", "ever", "overall", "kabhi bhi", "ab tak") is dropped with a note, and the question is answered over all data;
  • any other phrase the server cannot resolve (for example "pichle mahine" left in Hinglish instead of "last month") makes the plan unusable, so the rules planner's outcome stands, with a note: a question about last month is never answered over all data.

A plan you send yourself in cohort.plan is checked exactly as before.

Read-all answers: the same in every language, with the question's meaning kept

Asked "which customers discussed X" or "what do customers raise about X" in English and then in Hinglish, the read-all lane could list different customers, and two runs of the same question could too. 0.10.5 changes what the readers (the map, theme and check calls) are given:

  • A plain topical question gets one canonical English question. A question that only asks who discussed, mentioned, talked or asked about a topic (themes: what customers raise or say about it), with no negation, no and/both, no other verb or qualifier, and several topics joined only by or, is given to the readers as a question built from the plan's topic: "Which customers discussed loan or EMI?", whatever language it was asked in. Its topic is the plan's query words without function words, question words, generic lookup verbs ("discussed", "ki baat ki") and the words of a channel the plan's own conditions already select ("phone call"), with acronyms kept.
  • Every other question keeps its meaning. A negation ("which customers did not mention a refund?"), a conjunction ("discussed both insurance and a loan"), a specific verb ("asked to cancel their loan", "wanted a callback") or a reasons question ("why do customers cancel?") reaches the readers as asked: in the asker's words for an English question, and as the model planner's faithful English translation for a Hindi or Hinglish one (a translation that does not read as English, or lost the question's negation or and, is not used; the asker's words are). These questions never share a cached read with the plain question about the same topic.
  • The operator glossary is binding on a model-written read plan. A glossary phrase of the question whose facet the plan leaves out (for example "phone call" left out of a Hinglish question's plan, which would read every chat too) is added to the plan with a note, so the cohort is the one the rules planner builds for the same words in English. A facet the plan already names, also in other terms for the same channel, or whose words the model kept as the topic ("call back"), is left as planned. The planner is also told that a denial of what was said reads the same conversations as the question without it: "Kin customers ne phone call par loan ya EMI ki baat nahi ki?" was planned as "customers who never had a call" (3 conversations read) on an earlier 0.10.5 candidate, and now reads the same 574 calls as the English question.
  • A complete read is reused. The result of a read in which every conversation was read and none was erased while it was read is kept in the server's memory and served again, with no map, theme or check call, to a later answer with the same identity: the same tenant, scope, subject level, naming and alias key, the same plan over the same conversations at the same data revision, the same reader question, models, prompts and read settings, and the same caller authorization. Before a result is reused, every conversation it was computed from is checked again against the live forget and erasure fences; one that fails drops the scope's results and the cohort is read again. /v1/forget and /v1/erasures clear the whole cache before they run and when they finish. At most 16 results (32 MiB) are kept, least recently used out; nothing is written to disk and a restart empties it. naming: "alias_ephemeral" answers are never kept. The narration still runs for every answer, in the asker's language. CORTEX_COHORT_READ_CACHE=0 turns reuse off.

Measured on a copy of a 300-customer store whose cohort of phone calls holds 574 conversations (read-all on, map model claude-haiku-4-5, answer model claude-opus-4-8). On 0.10.4 the question "Which customers discussed a loan or EMI on a phone call?" listed 115 to 128 customers from one run to the next, and its Hinglish wording 132, because the model planner left out "phone call" and the chats were read too. On 0.10.5:

QuestionWhat the readers were givenResult
"Which customers discussed a loan or EMI on a phone call?""Which customers discussed loan or EMI?"574 of 574 calls read, 113 customers, 13.5 s, $1.25
The same, asked again(reused)the same 113, no map call, 6.4 s, $0.04
"Kin customers ne phone call par loan ya EMI ki baat ki?"(reused: the same plan)the same 113, no map call, 9.6 s, $0.07
The Hinglish question first, on another boot of the same copy"Which customers discussed loan or EMI?"574 of 574 calls read, 112 customers; the English question then reused this read: the same 112
"Which customers did not discuss a loan or EMI on a phone call?"the question as askedits own read of the 574 calls: 114 customers, 111 of them also in the first list (see "Known issues")
"Kin customers ne phone call par loan ya EMI ki baat nahi ki?""Which customers did not talk about loan or EMI on a phone call?" (the planner's translation)its own read of the same 574 calls: 111 customers
"Which customers discussed both a loan and insurance on a phone call?"the question as asked88 customers, 86 of them in the first list (the earlier candidate's canonical "loan or insurance" would have widened it)
The first question again, after one listed customer was erased"Which customers discussed loan or EMI?"the cache was cleared by the erasure: 571 of 571 calls read again, 114 customers; the erased customer, listed by the first read, is not among them

A fresh read of the 574 calls took 12-17 s and cost $1.24-1.28 in model calls; a reused one 6-10 s and $0.04-0.07 (the planner and the narration). The two fresh reads of the plain question (113 and 112 customers) show the run-to-run spread that reuse removes for repeated questions.

Reuse needs the same data revision, so on a scope that receives captures all the time a result is rarely reused; the canonical question and the glossary binding are what make two such reads agree. docs/ERASURE_COVERAGE.md lists the result cache as a store (no conversation text; covered by the clears and the fence re-check).

Settings page

  • A blank key field keeps the stored key. The 0.10.4 console already left a blank key out of a save, but the server still read a key sent as "" or null as "remove it", so a client that sent the field (the 0.10.3 console did) lost the key at the next restart. A blank or null key is now never written; the card's "remove it on save" box (clear) is the only way to remove one. A key an older console already removed stays removed until you re-enter it (see "Before you upgrade").
  • The restart banner clears after the restart. A connection is pending only while a saved value differs from the value the running process booted with, not whenever an override exists; pending_fields names the fields. The console no longer keeps its own "saved this session" flag.

Label limits (announced)

Before 0.10.5 nothing checked a label. 0.10.5 adds these limits on context.labels, not enforced by default: a cap on what clients send is announced before it applies. A later release will enforce them by default; an operator can enforce them now with CORTEX_LABEL_LIMITS=on.

LimitDefaultSettingError while enforced (422, not retriable)
Labels per memory4,096CORTEX_LABELS_MAX_COUNTLABELS_TOO_MANY
UTF-8 bytes per label262,144 (256 KiB)CORTEX_LABEL_MAX_BYTESLABEL_TOO_LONG
Not empty or only whitespacealwaysnoneLABEL_EMPTY
No ASCII control character except tab, line feed and carriage returnalwaysnoneLABEL_INVALID_CHARACTER
  • Turning them on: CORTEX_LABEL_LIMITS=on (also 1, true, yes, enforce) in the server's environment, then restart. The boot log says label limits: enforced on new writes.
  • Raising or lowering them: CORTEX_LABELS_MAX_COUNT and CORTEX_LABEL_MAX_BYTES (integers, at least 1). A value that is not a number, or is 0, keeps the default and is reported at boot (code=LABEL_LIMIT_INVALID). Raising them never breaks a client.
  • What is checked while they are on: every new write (POST /v1/experience, every /v1/experience/bulk item, every /v1/import row), after the pre-0.8 metadata.labels alias is folded into context.labels; stored events are never re-checked. details names the field, the label's index and the limit, never the label's text. A bulk batch with one failing item answers INVALID_ENVELOPE with the label code on that item and writes nothing; an import row fails alone.
  • Why these numbers: one pilot's ingester writes at most 15 labels of at most 58 bytes. The first-party connectors write one label per entity (a calendar invite carries one per guest) and carry metadata values in key=value labels (a Notion page's raw properties, an email's recipient lists) whose real sizes have not been measured, so the byte default is set far above anything seen. Before enforcement becomes the default it will be checked against real connector stores.
  • What to check now: labels that are empty or only whitespace, or that hold a control character other than tab, line feed and carriage return. cortexdb-connectors 0.2.22 writes an empty tag label when a provider leaves a field out (an Intercom conversation's state, part type or role, a Confluence space key, a Zendesk status); a later connectors release drops empty tags. The served schema (/v1/schemas/experience_envelope) states the limits in x-cortex-label-limits, not as constraints, so a client that validates against it accepts what a default server accepts.

API reference §3.5.3, "Label limits", has the full rules.

Docs

  • API reference §3.4.1 lists the scope types each preset allows: cloud_shared_saas (the hosted service) allows org, dept, team, app, user, agent, service, ws, project and source; on_prem_enterprise and cloud_private add global and system; dev_local adds debug and temp. §5.3.2 now shows the cloud_shared_saas preset as the server loads it.
  • API reference §8.5 warns that a forget without cascade runs derived_only on every deployment, the hosted one included, and keeps the memories; it shows how to delete them (selector.memory_ids, /v1/erasures) and which deletions free the writes' idempotency keys (a memory_ids forget and an erasure do; a scope-wide redact_events forget does not).
  • directives.extract is documented as a list of stage names: omitted or null runs every stage, [] runs none, and false or a string is 422 INVALID_BODY. It is honoured on single and bulk writes (since 0.10.3).
  • docs/BEST_PRACTICES.md follows all three, and its example scopes are valid paths.

Measurements

Measured on two release candidates of this code: 1c5acd42 (every check below) and 8fa30280, which only adds the planner instruction about denials (described under "Read-all answers"; its image was smoked, and the Hinglish questions were measured again on it). Only these notes changed after 8fa30280.

  • Tests. The workspace suite (cargo test --workspace --all-targets) on Linux at umask 022, three runs of this code: 8,431 passed, 0 failed, 88 ignored (210 test binaries) twice; the third had one failure in a storage test of the dormant native subsystem, which this release does not touch, while the image smoke and the live Docker suite ran beside it on the same machine (the test passed 20 of 20 times alone, and its crate's suite 5 of 5). Every run includes the English regression gate (8 of 8, with the 64-combination cross-customer flag matrix) and no WAIT_TIMEOUT anywhere. Also green on 1c5acd42: rustfmt, the clippy ratchet (no new warning), doc tests, the script gates, the Python SDK, CLI, MCP (with a clean install) and TypeScript suites, the connectors, the server, backup and restore, retention, WAL-migration, usage-ledger, tenant-isolation, commitment and native-host smokes, and the multilingual mock evaluation with its top-5 diff and the kiraya check. The legacy-WAL restart smoke (not a CI gate) failed once under that load and passed 30 of 30 checks on a rerun; it fails the same way on 0.10.4.
  • Image. Both candidates' multi-arch images boot on amd64 (ready in 1-2 s) and on arm64 under QEMU (6 s) with version 0.10.5, their own commit, the same 11 capabilities as 0.10.4 and these notes bundled; the multilingual and lifecycle smoke passed 42 of 42 and the cohort smoke 23 of 23 on each, and the live Docker default matrix 43 of 43 on each.
  • Erasure on a copy of a 300-customer store (about 130,000 events, first boot of the copy): see the table under "Fixes". Both proofs were clean: 13 of 13 stores, every erased id answers 404, no file holds the canary's text or label.
  • Read-all on the same copy: see the table under "Fixes".
  • Single-customer answers do not change. The 115-question customer-journey set, replayed without the answer model in two request shapes: 113 of 115 contexts are byte-identical with two separate boots of v0.10.4. One of the other two also differs between those two v0.10.4 boots (a ranked cut-off that varies per boot); the other has the same size and one different event at a ranked cut-off, a per-build difference seen between builds of 0.10.3 as well (no code path this release changes is involved). One question per category, answered by the model: 12 of 12 correct.
  • wait=indexed. About 51,600 ?wait=indexed writes against an earlier candidate of this release with the same wait code (sequential and concurrent writers on an otherwise idle server, and a concurrent write, seed and forget load): no 408, and the slowest write took 2.4 s.
  • Model spend for all of this release's measurements on the store copy: $11.54.

Environment variables

VariableDefaultWhat it does
CORTEX_COHORT_READ_CACHEonRead-all result reuse (see "Read-all answers"); 0 turns it off.
CORTEX_LABEL_LIMITSoffon enforces the label limits on new writes.
CORTEX_LABELS_MAX_COUNT4096Labels per memory while the limits are enforced.
CORTEX_LABEL_MAX_BYTES262144UTF-8 bytes per label while the limits are enforced.

docs/generated/env-vars.md has the details.

Error codes

HTTPCodeWhereRetriable
422LABELS_TOO_MANYA write with more than CORTEX_LABELS_MAX_COUNT labels, only while CORTEX_LABEL_LIMITS=onno
422LABEL_TOO_LONGA label longer than CORTEX_LABEL_MAX_BYTES UTF-8 bytes, only while enforcedno
422LABEL_EMPTYAn empty or whitespace-only label, only while enforcedno
422LABEL_INVALID_CHARACTERA label with an ASCII control character other than tab, line feed and carriage return, only while enforcedno

On /v1/experience/bulk the code is on the failing item inside a 422 INVALID_ENVELOPE.

Startup warnings

  • SETTINGS_OVERRIDE_CLEARS_SECRET: an entry of settings_overrides.json unsets an API key the launch environment sets (what an earlier console wrote when a card was saved with its key field empty). Re-enter the key in Settings, or delete the entry from the file.
  • LABEL_LIMIT_INVALID: CORTEX_LABELS_MAX_COUNT or CORTEX_LABEL_MAX_BYTES is not a number or is 0; the default is used.

Rollback

A 0.10.4 image opens a data dir 0.10.5 has used: no column family, directory, WAL row kind, index field or key prefix is added, and the read-all result cache is in memory only. A Settings save that cleared a key writes the same file entry 0.10.4 writes for a removal. To roll back, stop 0.10.5 with the usual 120 s grace and start 0.10.4 on the same data dir. After a rollback, a Settings save with an empty key field from a client that sends the field removes the key again (the 0.10.4 behaviour), the label limits do not exist, and erasures on a store with expired retry records are slow again.

Checked on one data dir with the published v0.10.4 image and each release candidate, the cohort flags on, a graceful stop between each step (0.10.4 writes, 0.10.5 reads and writes, 0.10.4 again, 0.10.5 again): no column family, directory or on-disk format was added (the only new top-level entry, cohort_alias.key, comes from 0.10.4's cohort lanes), no error line and no panic, and every data check passed (84 of 85 checks; the remaining one expected the 404 a 0.10.3 rollback answers on /v1/cohort/query, where 0.10.4 answers 409 COHORT_DISABLED with its flags off, as it should).

Known issues

  • A fresh read is still the map model's reading of each conversation. Reuse makes repeated answers over unchanged data agree; it does not make a reading deterministic. Two fresh reads of the same cohort can list a few different customers (in the release measurements, two fresh reads of the same question listed 113 and 112 customers). Reuse needs the same data revision, so on a scope that receives captures continuously most answers read afresh.
  • A negated read-all question is read conversation by conversation. "Which customers did not discuss X?" lists the customers with at least one conversation that does not discuss X, as each conversation is read; it is not "customers none of whose conversations discuss X". For that set, ask the positive question and take the rest of the cohort.
  • The planner's translation is a model's translation. A non-plain Hindi or Hinglish question reaches the readers as the planner's English translation; the server uses it only when it reads as English and keeps the question's negation and and, and otherwise passes the asker's own words, but it cannot check every other nuance. cohort.read.reader_question shows what the readers got.
  • Two identical read-all answers at once read the cohort once. The second waits for the first (at most until 25 s before its own answer budget ends) and reuses its result. If the first stores nothing (a partial read, a result above 4 MiB, an answer cancelled late) and fewer than 10 s of read time are left after the wait, the second answers from the ranked sample with the note read: all waited for another answer ... instead of reading every conversation. Ask again for a full read.
  • Label limits are not enforced by default (see "Label limits (announced)"). While enforced, a retry of a write the server accepted before enforcement was turned on, whose labels break a limit, answers 422 instead of replaying the earlier write: the limits are checked before the retry key is looked up. Turn enforcement on only after your clients' labels pass.
  • A Settings key removed by an older console stays removed until it is re-entered (see "Before you upgrade").
  • Everything listed under "Known issues" in RELEASE_NOTES_0.10.4.md still applies.

Clients

No client release. Every client published with 0.10.4 (Python SDK cortexdbai 0.12.0, TypeScript SDK cortexdbai 0.10.1, CLI cortexdb-cli 0.6.0, MCP cortexdb-mcp 0.8.0, connectors 0.2.22) works against 0.10.5 unchanged. In source, the Python SDK's experience(), the TypeScript ExperienceOptions.labels comment and the MCP server's memory_store tags parameter describe the announced label limits (docstrings only; no version change).

Notes

Single-node remains the only supported topology.

v0.10.4

questions across the customers under a parent scope (experimental, off by default), an honest note on parent-scope packs, and your request id in every error

0.10.4 is a feature release on top of 0.10.3, the last published release. Everything in RELEASE_NOTES_0.10.3.md still applies; this page lists only what changed since. It adds no column family, directory, WAL row kind, index field or on-disk format (one key prefix, erasure_job/, for the erasure receipts in an existing column family), and nothing is migrated: a 0.10.3 server opens and serves a data dir 0.10.4 has used (see "Rollback").

Full notesHide notes98 notes

Summary

  • Questions across the customers under a parent scope (experimental, off by default). Asked at a parent scope such as org:acme about the customers under it (org:acme/user:*), a question used to be answered from whichever customers' events came first in storage order: in practice the oldest events of one or two customers, whatever the question said. 0.10.4 adds two opt-in lanes beside that path. Each needs an operator flag and an explicit request field or route, so nothing changes until both are in place:
    • the exact lane (CORTEX_COHORT=on) answers lookups, counts, top-N lists, funnels, segments, sequences, shares and "latest" questions over the typed facets of every customer the caller may read (event intent, labels, modality). It is computed from the WAL with every forget and erasure applied, through POST /v1/cohort/query (no model call) and the /v1/answer field cohort (one narration call whose numbers the server checks);
    • the ranked lane (CORTEX_SUBTREE_RECALL=request) runs one relevance-ranked recall across the authorized child scopes (/v1/recall field subtree). On /v1/answer it serves content lookups ("which customers asked about refunds in a chat") and syntheses over conversations ("what do customers ask about financing"). By default such a question now READS EVERY CONVERSATION of its cohort (lane: read_all): bounded parallel model calls read each conversation once, the server counts the matching customers and the customers behind each theme, the answer model re-reads each listed theme's example conversations before any is cited, and one narration states them, its numbers checked; coverage says "read N of N conversations". CORTEX_COHORT_SYNTH_MAX_CALLS=0 (or cohort.read: "sample") keeps the cheaper ranked sample of excerpts, stated as a sample.
  • Every customer is named by a stable alias to the model and by its real scope to you. Answers carry the plan, the window, the time anchor, the coverage, per-customer counts and the evidence event ids, each with its own scope.
  • Measured on a 30-question cross-customer set (31 items with an anchored variant, frozen ground truth, a 300-customer test store): through /v1/answer the exact lane answered 19 of 19 exact-lane questions exactly, and the 6 attribute questions answered from labels added at ingest 6 of 6; 0.10.3 answered none of the 31. Of the 6 questions about what customers said, now answered by reading every conversation of their cohort, the content lookup passes its bar (F1 0.80 against 0.8) and the five syntheses read all of their cohort and state their coverage but stay below their bar (3, 2, 3, 1 and 4 of the ground truth's top-5 themes; 27 of 42 cited examples carry their theme). Treat the conversation lanes as previews. Details under "Measurements".
  • The cohort lanes never reach a scope with no children, with the flags off or on. The English regression gate holds under every flag combination, and the 115-question customer-journey set produces the same answer contexts as 0.10.3 (112 to 114 of 115 byte-identical per run; every difference is one of three questions whose ranked cut-off or order also varies between boots and builds of 0.10.3 itself). One single-customer change is opt-in: Hindi and Hinglish "latest" / "last" words for the inferred newest-first order (CORTEX_RECENCY_CUES_MULTILINGUAL, off by default).
  • Parent-scope packs say they are storage-order samples (on by default). A recall or answer at a parent scope that was served from the storage-order walk now carries one warnings[] note, and the answer prompt one more line. CORTEX_PARENT_SAMPLE_WARNING=0 turns it off.
  • Enrichment can be confined to chosen scopes (CORTEX_ENRICHMENT_SCOPE_PREFIXES, unset by default), facts name the model that extracted them (extractor), events list their facts (derives), and the admin surfaces report the extraction model actually running. Details under "Enrichment on chosen scopes, and where each fact came from".
  • Erasure with proof. POST /v1/erasures now writes a gdpr audit row (its id is in the 202), a durable receipt (ids, counts, timestamps and hashes, never content) that survives a restart, and an erasure_complete lifecycle row its lifecycle_stream link delivers. Two new routes: GET /v1/erasures/{id}/verify (trusted operator) re-reads every store for what the erasure removed and answers one check per store plus all_ok; GET /v1/erasures/{id}/receipt serves the receipt with its SHA-256. The redacted copies of an erased event no longer keep its labels and intent, GET /v1/audit lists newest first, and the ranked lane no longer counts a customer whose events were all erased. Details under "Erasure: receipts and proof".
  • Cross-customer answers say where their time went and what they cost. A cohort answer carries diagnostics.cost_usd (its model calls at the price book's rates), a finer diagnostics.time_ms split and a Server-Timing header that separates CortexDB's own time (cortex) from the model's (llm); CORTEX_COHORT_WARM_ROOTS builds named parent scopes' projections at boot. Details under "Timing and cost".
  • The admin console uses all of it. Console › Ask across customers splits the wait into CortexDB, model and network, shows the cost and the read-all coverage, can answer without the model and re-run a plan without it; Console › Recall can search across customers; a presentation mode shows customers by alias only. Memories shows which model extracted each fact and from which event, a new Re-derive page re-extracts one customer's facts with another model and shows before and after, a new Erasure page erases one customer and shows the proof and the receipt, and Audit lists newest first. Details under "In the admin console".
  • One time budget per cross-customer answer. Planner, reads, model calls and narration all finish inside CORTEX_COHORT_ANSWER_BUDGET_MS (85 s by default, at most 95 s), under a 100 s proxy origin limit; measured answers took 2.5-7.0 s (exact answers) and 13-48 s (reading 52-626 conversations).
  • Every error envelope carries the request's own id, in the body and in X-Request-Id.
  • /v1/recall 422s say which field was wrong (details.part, details.reason, details.field_path), as /v1/answer does.
  • scripts/cold_backup.py restores ownership as 1000:999, the image's cortex user and group.
  • No client changes. Every client released with 0.10.3 works against 0.10.4 unchanged; none of them sends the new fields yet.

Upgrade from 0.10.3

Before you upgrade: what clients and operators will notice

With every new flag at its default (the cohort lanes off), these are the only changes:

  • Parent-scope recalls and answers (CORTEX_PARENT_SAMPLE_WARNING, on by default). Only for requests at a scope whose child scopes hold data, and only when the pack's child events came from the storage-order walk (never at a leaf, never on a ranked subtree pack):
    • the /v1/recall pack (and the pack /v1/answer, /v1/compose and use_pack_id reuse) gains warnings: ["parent_pack_unranked_sample: events from K of M authorized descendant scopes, selected in storage order, not by relevance"] (or events from K authorized descendant scopes, ... when M is not known). K counts the descendant scopes the final pack holds;
    • a /v1/answer built from such a pack adds one line to its prompt: "The memories below come from K scopes under this one (customers, teams, sessions or similar) and were selected in storage order, not by relevance: they are a sample, so do not generalise them to every scope under this one." K is counted over the context the model reads. The answer text can change accordingly: it should stop presenting one child's history as "the customers'";
    • nothing else changes. The note is not a diagnostics.degraded_sources entry, it does not set diagnostics.partial, and such answers stay cacheable under CORTEX_ANSWER_CACHE. A client that treats any warnings[] entry as an error should treat this one as informational.
    • Kill switch: CORTEX_PARENT_SAMPLE_WARNING=0 (or off, false, no) restores 0.10.3's packs and prompts byte for byte.
  • Error envelopes echo your request id. The errors a route's handler returns (experience, recall, layer, lifecycle, event page, auth and others) used to mint a fresh req_<uuidv7> for the body and put it in X-Request-Id, replacing the id the client sent. Now the body's request_id, X-Request-Id and X-Cortex-Request-ID (when present) all carry the request's own id. Consequences:
    • when the client sends X-Request-Id, request_id is that value: do not treat it as a server-unique id, and do not expect the req_ prefix (docs/schemas/error_envelope.schema.json no longer has the req_ pattern);
    • the audit row, the request log line, the header and the body still join on one id.
  • /v1/recall 422 INVALID_BODY now carries details.part, details.reason and details.field_path, like /v1/answer. Status, message and success responses are unchanged.
  • Fact provenance on the layer listings. A GET /v1/events item that live facts cite (an enriched capture, or a typed triple's own event) gains derives with their ids, and a GET /v1/facts item extracted from now on gains extractor; an event or fact without them keeps its old shape. GET /v1/admin/layers/stats reports the extraction model the running router was built with (it used to report the model the Settings page had just saved). Recall and answer packs do not change.
  • Erasures (see "Erasure: receipts and proof"): the POST /v1/erasures 202 gains audit_id, receipt_url and verify_url; GET /v1/erasures/{id} gains audit_id, created_at and completed_at, and answers after a restart; the preview's estimated_duration_ms is 20 to 35 times smaller (1 s plus 2 ms per event, was 250 ms plus 80 ms); the returned lifecycle_stream now carries only that erasure's row; a redacted event loses its labels and intent along with its content (the WAL twin of every erased event, and the v1 row of a descendant of a whole-scope erasure or of a /v1/forget with cascade: redact_events, which also loses its location). GET /v1/audit lists newest first: a client that read the first page as the oldest rows must sort or page differently.
  • scripts/cold_backup.py documents and restores the image's owner as 1000:999 (user cortex, group cortex), not 1000:1000. Use chown -R 1000:999 on a restored dir for the image.
  • Enrichment-on deployments only (nothing changes with enrichment off, the default):
    • with CORTEX_ENRICHMENT_LONG_TEXT_WINDOWING on and CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT unset, the per-event attempt budget is now 4 per window, at least 16 (it was 8), which doubles the most a failing long event can spend on retries. To keep 8, set CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT=8 (see Fixes);
    • a small batch of enrichment jobs now runs its jobs concurrently on the job permits free at that moment (it ran them one after another), so a burst of a few long events reaches the provider in parallel. CORTEX_ENRICHMENT_CONCURRENCY still bounds the jobs in flight: set it lower to spread them out.
  • Non-English "latest" questions on one customer: unchanged unless you opt in. The Hindi and Hinglish recency words are read only with CORTEX_RECENCY_CUES_MULTILINGUAL=1. With it on, such a question can serve the newest-events tail instead of ranked retrieval (about 3,600 more input tokens, about $0.018 at the opus rate, per affected answer); CORTEX_RECENCY_INFERENCE=0 still turns all inference off.
  • The served contract grows, additively. docs/schemas/openapi.json documents the six new routes (the four cohort routes, and the erasure /verify and /receipt), the new fields and the Server-Timing header of the cohort responses; answer_response.schema.json documents context_block, the optional cohort block, diagnostics.cost_usd and the citation fields scope and subject; stratified_pack.schema.json documents the two new warning notes and the optional subtree summary. None of them appears on a response unless the request opted in, apart from the parent note above.

Upgrade steps

Nothing is migrated and there is nothing to run afterwards. Replace the binary or image and restart as for any release, giving the server 120 s to stop. Then check:

  1. GET /v1/admin/version: crate_version 0.10.4 and the git_sha you deployed. The capability list is the same 11 names as 0.10.3 (the two cohort names are known but not advertised; see "Detecting support").
  2. GET /v1/admin/ready: ready: true.
  3. If a client of yours treats any warnings[] entry as an error and calls parent scopes, either let it ignore parent_pack_unranked_sample or set CORTEX_PARENT_SAMPLE_WARNING=0.
  4. To try the cohort lanes, see "Turning the lanes on".

Upgrading from 0.10.2 or earlier

0.10.4 contains everything in 0.10.3. Read RELEASE_NOTES_0.10.3.md, "Upgrade from 0.10.2" (and from there the earlier notes) first; this page applies on top.

Questions across customers (experimental)

API reference: §8.3.1 (subtree on /v1/recall), §8.4.2 (cohort on /v1/answer), §8.4.3 (POST /v1/cohort/query, GET /v1/cohort/vocabulary, the plan grammar and the cursor), §8.15 (POST /v1/admin/cohort/warm, GET /v1/admin/cohort/status), §9 (pack warnings and subtree). Operator settings: docs/generated/env-vars.md §9.2.

Why

A recall at a parent scope reads the child scopes' events in storage order, takes the first 10,000, and the token budget then keeps a few dozen of them. The query plays no part, so every question at the parent got the same pack, usually from one customer, and /v1/answer either abstained or described that one customer as "the customers". No existing setting (filters, views, recency order, the answer's supplemental passes) answers "which customers did X", "how many" or "what do customers say about Y" correctly at a parent. The new lanes route each kind of question to a mechanism that can answer it, and refuse what they cannot answer.

Turning the lanes on

StepWhat
Exact laneCORTEX_COHORT=on in the server's environment. Needs the unified WAL (the default); a legacy-WAL server answers 503 COHORT_REQUIRES_UNIFIED_WAL.
Ranked laneCORTEX_SUBTREE_RECALL=request as well. The ranked and synthesis questions on /v1/answer need both flags.
Read allOn with the ranked lane: a synthesis or a "which customers said X" question reads every conversation of its cohort (CORTEX_COHORT_SYNTH_MAX_CALLS, default 32 map calls; 0 reads the ranked sample instead). It spends map-model tokens per question, about the cohort's conversation text; CORTEX_COHORT_SYNTH_MAP_MODEL picks a faster, cheaper model for it.
CallerThe scope.read.descend capability on the parent scope (the static operator key has it). Every count and list covers only the customer scopes the caller may read.
Request/v1/answer with "cohort": {} (or with options), /v1/recall with "subtree": {}, or the /v1/cohort/* routes. view must be holistic, descend or absent.
Warm-up (optional)POST /v1/admin/cohort/warm {"scope": "org:acme"} builds the scope's projection in the background ahead of the first question, and CORTEX_COHORT_WARM_ROOTS=org:acme does the same at boot; until it is ready a question answers 503 COHORT_WARMING with Retry-After.
Glossary (optional)<data_dir>/cohort_glossary.json maps your business phrases to facets ("paid for an order" -> intent shop.order.payment.done). Configuration, never customer data; read on each cohort request.
CheckGET /v1/admin/cohort/status shows the switches, the knobs, the resident projections and their counters, whatever the flags say.

The flags are read from the process environment only: the admin console's settings and feature overrides do not reach them. Both switches are read on every request; the numeric knobs once, at start.

Detecting support. cohort_query_v1 and subtree_recall_v1 are known names but are not listed in GET /v1/admin/version capabilities[] in 0.10.4, so do not gate on them yet. Probe with POST /v1/cohort/query instead: 404 means a server without the lanes (0.10.3 or earlier), 409 COHORT_DISABLED means the flag is off, anything else means the lane is served. An older server ignores an unknown cohort field on /v1/answer and answers from the storage-order sample, so treat a 2xx answer without a cohort block as "no cohort lanes here" and never show its text as a cohort answer.

In the admin console. Console › Ask has an Across customers switch next to the scope. When it is on, the question is asked at a parent scope (by default the root of the working scope, so org:acme for org:acme/user:u1; the field is editable) with cohort, and the page shows the narration above the structured block: the lane, the plan in words with the planner's notes, the coverage ("read N of N"), the customers table (alias, real scope, counts, cited), the top-N, funnel and segment tables, and citations that open each event at its own scope. It detects support as described above, words every refusal (409, 422, 503), waits out 503 COHORT_WARMING, and has no client timeout, so a question that reads conversations can take its time (8-48 s measured on 52-626 conversations, inside the 85 s answer budget). When the switch is off, Ask sends exactly the request it sent in 0.10.3. What the console shows of the time, the cost and a read-all answer is under "In the admin console".

What each lane answers

QuestionLaneExact?Example (synthetic shop)
Which customers did XExact listYes, complete over the authorized customers"Which customers cancelled an order?"
None did XExact listYes: an empty set with its coverage ("0 of 240")"Which customers viewed a Zephyr?"
How manyExact countYes"How many customers confirmed an order last week?"
Top NExact top (by customers or events; by intent, label family, day or week)Yes; ties at the cut-off are listed"Top 5 titles by viewers"
FunnelExact funnel (2-8 steps, optionally ordered)Yes"Order funnel: placed, paid, delivered"
SegmentsExact all_of / none_ofYes"Viewed both X and Y"; "confirmed but never had a conversation"
SequenceExact sequenceYes"Cancelled, then later confirmed"
ShareExact shareYes"What share of views were hardcover?"
LatestExact latestYes"The 10 newest orders, one per customer"
Attribute joined through an identifierExact, with a via join the planner infers from the vocabularyYes, when the attribute is a label"Viewed paperbacks but ordered a hardcover"
What customers saidRead all: every conversation of the exact cohort the question names is read, and the customers whose conversations show it are listed (read: "sample" or read-all off: ranked, one or two excerpts per customer)Read all: complete over every conversation read, as the model reads each one; sample: no, coverage stated"Which customers discussed refunds in a chat?"
Themes across conversationsRead all: every conversation read, the themes grouped and counted by customer by the server (read: "sample" or read-all off: synthesis over one excerpt per customer)Read all: counts complete over what was read, themes as the model reads each conversation; sample: no, coverage stated"What do customers ask about financing?"
One customerFocus: the alias from an earlier answer, answered by the single-customer pathAs today"What did C-4F2AQ7M do?"
Anything the planner cannot mapNoneRefused with a reason, no model callunknown_term, needs_text_attribute, needs_conversation_reading

The exact lane only knows typed facets: an event's context.intent, its labels and its modality. An attribute that lives only in event text (a colour, a size, a city written in a sentence) cannot be counted exactly; label it at ingest. On the measured store, six attribute questions that 0.10.3 could not answer became exact once those attributes were written as labels.

The planner. /v1/answer maps the question to a plan with deterministic rules (CORTEX_COHORT_PLANNER=rules, the default): it matches the words against the caller's authorized vocabulary (intents, label values, label families, modalities, the glossary), reads time windows (in June 2026, between ... and ..., last week, past 30 days, resolved against the anchor), clauses, negation and sequence words, and refuses with unknown_term rather than drop a word that could narrow the cohort. CORTEX_COHORT_PLANNER=llm adds one JSON-plan call (the model sees the question and the vocabulary, never scopes, event ids or event text). CORTEX_COHORT_PLANNER=auto, recommended when users ask in more than one language, runs the rules first and makes that call only when they cannot plan the question (unsupported, unknown_term above all) or the question is not English (Latin-script Hinglish, Devanagari, any non-Latin script): an English question the rules can plan gets exactly the rules plan and answer, with no model call, and cohort.notes says why the model planned when it did. An explicit cohort.plan always wins. Every answer echoes the plan and how each phrase was mapped (cohort.notes), so a reader can see that "cancelled" meant intent shop.order.cancel. GET /v1/cohort/vocabulary lists what can be matched.

The time anchor. Relative windows resolve against cohort.anchor: now, data_end (the newest authorized event under the scope) or an RFC 3339 instant. A store whose data ended weeks ago answers "last week" with data_end correctly and with now as an empty week, and both say which anchor and window they used.

Aliases and attribution

  • The model sees customers only as aliases, C- followed by 7 characters (for example C-4F2AQ7M), derived per parent scope from a key in <data_dir>/cohort_alias.key. The same customer keeps the same alias across requests, so a follow-up can name it (cohort.focus). naming: "alias_ephemeral" uses per-request aliases instead, and naming: "scope" has the server replace each alias in the answer text with the customer's scope path after the model call.
  • The model never sees a scope or an event id. The caller always gets the real scopes: cohort.result.subjects[] lists each customer's alias, scope, matching event count, first and last date and evidence event ids, with evidence_scopes giving each evidence event's own scope (an event can sit below its customer, in a session for example).
  • Citations carry scope (the cited event's own scope) and subject (the alias the model saw). Every id reads back with GET /v1/events/{id}?scope=<that scope>.
  • cohort.result is the structured answer and the thing to display or grade. On the ranked lanes it lists the customers whose excerpts were read, and each row's cited flag says whether the narration cites that customer: the model's reading of a sample, never a complete list.

Coverage and the number check

  • cohort.status is complete (exact over every authorized customer), partial (a ranked or sampled read) or unsupported (with cohort.unsupported.reason). cohort.coverage states the customers and events considered, the data span, the window and the anchor; on the ranked lanes also the customers read out of the customers in the cohort and, for a synthesis, the conversations read out of the conversations in the cohort.
  • After the narration call the server checks every number in the answer (digits in any script, percentages, dates, times, number words, counts written 12.4k or 3 lakh) against the numbers it computed, in the role the answer uses it in: a count against totals, a group's value only next to its own key, a customer's count only in a sentence naming that customer's alias, a share with rounding, a date only as a date. A number from the question counts only when the plan used it, and text in an excerpt never counts. On the exact lane any number that fails replaces the narration with the server's own summary; on the ranked lanes the sentences stating it are removed (fail closed when more than half would go). Either way the numbers are listed in cohort.narration_unverified_numbers and diagnostics.degraded_sources gains cohort_narration_unverified.
  • The check has known limits: inside one clause a key and a value are paired by position, so "Quill and Harbor had 179 and 184" without "respectively" pairs them in order; and a count that happens to equal another count of the block verifies.

Refusals

The lanes refuse rather than answer from a sample presented as complete:

ConditionResponse
A flag is off409 COHORT_DISABLED / SUBTREE_RECALL_DISABLED
The caller lacks scope.read.descend403 POLICY_DENIED (a /v1/recall subtree instead falls back to today's pack with the warning below)
view other than holistic / descend, cohort with stream or filters, an option out of range422 INVALID_BODY (a /v1/recall subtree with another view falls back with the warning below)
No authorized customer scope under the scope holds data (a leaf, for example)422 COHORT_NO_DESCENDANTS
More than CORTEX_COHORT_MAX_EVENTS events under the scope, or a projection larger than CORTEX_COHORT_MAX_BYTES413 COHORT_SUBTREE_TOO_LARGE (nothing cached)
The projection is being built or repaired after a forget, or the build queue is full503 COHORT_WARMING with Retry-After; stale data is never served
A question the planner cannot map200 with cohort.status: "unsupported", the reason and a pointer to the vocabulary; no model call
The ranked lane cannot run (the scope-partition map still building after a restart, more than CORTEX_SUBTREE_EXACT_VECTOR_MAX vectors in the subtree, an unsupported store)/v1/recall: today's pack plus warnings: ["subtree_recall_unavailable: <reason>"]; /v1/answer: unsupported with degraded_sources: ["subtree_recall_unavailable"], never narrated from the storage-order sample
The ranked lane is at its in-flight cap or deadline503 SUBTREE_BUSY with Retry-After

Safety

  • Tenancy and access. Access is decided per customer scope (per exact event scope on the exact lane), from the same revision-checked authorization snapshot the rest of the read path uses, re-checked before the response. Counts, lists, the vocabulary and the planner's view cover the readable customers only, and nothing reports how many were left out. The ranked lane keeps the three read-path fences: authorized partitions only, the tenant restriction inside the full-text query, and the stored-tenant check on every hit.
  • Forgets and erasures. Every row passes the same visibility checks as a recall (forgets, redactions, cancels, exclusions) plus the per-event erasure markers. Before every answer the projection is caught up with new writes and repaired for exactly the customers a forget or erasure touched; an untracked change forces a full rebuild in the background.
  • Nothing is stored. The projection lives in memory only and is rebuilt from the WAL, so no derived copy can survive an erasure (docs/ERASURE_COVERAGE.md). The answer cache never stores or serves a cohort answer.
  • The model provider sees aliases and, on the exact lane, counts and dates; on the ranked lanes the excerpts it narrates; on the read-all lane every conversation of the cohort (map calls, with no alias, scope or event id: only the question, its topic words and the text), the map's short labels (theme call) and the computed block (narration). With CORTEX_COHORT_PLANNER=llm the planner call also sends the vocabulary: intent names, label family names and the values of families with at most 64 values. Do not enable it if a label family holds customer identifiers.

Performance

Measured on the 300-customer test store (about 129,000 events and 139,000 vectors) in a 3-vCPU, 3 GB container:

  • Exact lane. A cold projection builds in the background in 2.4-2.5 s and takes about 33 MB (the process grew by 45 MB). Warm, POST /v1/cohort/query answers in 9 ms at p50 (23 ms p95) with a plan and 55 ms (71 ms) with a question; GET /v1/cohort/vocabulary in 45 ms. A 1,000,000-event, 10,000-customer projection measures 146 MB; with 8 concurrent readers and repeated refreshes the process grew by 86 MB, less than one projection, because refreshes are applied in place and never copied.
  • Ranked lane. /v1/recall with subtree at the 300-customer parent: 82 ms p50 and 92 ms p95 client-side, of which the search is 60 ms; about 13 customers per pack. The first ranked recall after a boot pays for a cold embedding call (1-3 s). The scan runs on its own small worker pool, so leaf recalls keep theirs: under two concurrent ranked parent recalls, leaf answer contexts were served 3-5 % slower at p50 and within run-to-run noise at p99.
  • /v1/answer with cohort. One model call per answer (two with the LLM planner). With the answer model used for the measurement (claude-opus-4-8): exact-lane answers took 4.5 s at p50, with 1,200-3,200 input and 160-700 output tokens ($0.010-0.029 each); ranked lookups and syntheses took 10-17 s, with 9,200-13,500 input and 580-860 output tokens ($0.065-0.085 each). Planning and execution take under 100 ms; the rest is the model call.
  • Read all. With claude-haiku-4-5 as the map model (8 map calls in flight, 40,000-token / 20-conversation batches) and the check and narration calls on claude-opus-4-8, the final pass read 52 conversations in 13-16 s end to end, 165 conversations in 22 s and 574-626 conversations in 28-48 s (33 map calls, one theme call, six check calls in parallel, one narration of 7-10 s); the development passes of the same logic ranged 8-45 s. Cost per question: $0.18 (a lookup) and $0.89-2.11 (a synthesis): the map's input tokens are about the cohort's conversation text (about 2,100-3,000 tokens per conversation on that store), and the check calls add $0.3-0.6 at the answer model's rate. With claude-sonnet-5 or the answer model as the map model the answers took about as long on 52-165 conversations and cost 1.4x and 2.6x as much.
  • Writes are unaffected. New-scope wait=indexed captures under concurrent forgets: p99 282 ms on 0.10.3 and on 0.10.4 with the lanes off or on (an earlier candidate of this release; the final fixes do not touch the write path).

Timing and cost

API reference §8.4.2 ("Timing and cost") and §8.4.3.

  • diagnostics.cost_usd on a cohort answer: every model call of the answer (the planner, the narration, read-all map and theme calls) at the price book's rates, the ones the usage ledger records the same calls at. 0 when no model was called, absent when a model has no rate (declare it in CORTEX_MODEL_PRICES). On read_all it equals cohort.read.cost_usd. A leaf answer never carries it.
  • diagnostics.time_ms splits cohort.exec: cohort.exec.cohort_filter (the exact cohort) and cohort.exec.ranked_recall on the ranked lanes, followed by the ranked search's own stage times (subtree.wait, .candidates, .search and .search.*, .visible_subjects, .hydrate; milliseconds only, never the counts); cohort.read_all.fetch, .map, .themes, .check and the per-call .map.call_p50 / .map.call_max on a read-all answer. llm_ms still counts only the planner and the narration: the model's whole share is the header's llm.
  • Server-Timing on the 200s of POST /v1/cohort/query, GET /v1/cohort/vocabulary and cohort answers: cortex;dur=...;desc="CortexDB", plan, exec, llm;desc="model", total, in ms with one decimal, cortex = total - llm. Every other response, leaf answers and recalls included, has no such header.
  • One budget for the whole answer. CORTEX_COHORT_ANSWER_BUDGET_MS (default 85,000, clamped 40,000-95,000) runs from the request's start and bounds every model and store call of a cohort answer: the LLM planner call gets at most 15 s, the read-all map and theme calls end by CORTEX_COHORT_SYNTH_DEADLINE_MS (now at most 60,000) and at least 35 s before the budget ends, the check calls at least 25 s before it, and the narration by it. A narration that misses it answers with the server summary (cohort_narration_failed), never a 5xx, so a cohort answer stays inside a 100 s proxy origin limit (Cloudflare's) even under a slow provider. An abandoned map call is still billed by the provider and recorded in the usage ledger.
  • CORTEX_COHORT_WARM_ROOTS (comma-separated scopes, unset by default) queues each named scope's projection build when the server starts, as POST /v1/admin/cohort/warm would, so the first question about it does not wait (about 2.5 s for the 300-customer test root). Each root is logged; a root the server refuses (COHORT_DISABLED, COHORT_WARMING, or INVALID_BODY for an entry that is not a scope) is logged with its code and nothing else happens.
  • benchmarks/latency/cohort_latency_bench.py measures plan, question, answer and ranked recall latency: p50 and p95 per question type of the server time (recall_total, Server-Timing), the round trip and the model's share, and the cost. It defaults to a built-in synthetic spec, takes --spec for your own questions, and refuses a server that is not on loopback unless --live.

Enrichment on chosen scopes, and where each fact came from

  • Enrichment scope allowlist (CORTEX_ENRICHMENT_SCOPE_PREFIXES, unset by default). A comma-separated list of scopes: while it is set, LLM enrichment runs only for captures in those scope subtrees, so enrichment can be turned on for a few customers of a store that must not spend on the others. It gates the deferred scanner, the retry set, the write-time dispatch, memory consolidation and the re-derivation route (POST /v1/admin/derived/rederive answers 422 ENRICHMENT_SCOPE_NOT_ALLOWED, at the dry run too, when a job would re-enrich an event outside it). With it set, the compaction pass's whole-store LLM stages no longer switch on with enrichment. A capture outside it is passed over for good (widening the list does not backfill). Unset, nothing changes.
  • Facts name their extraction model. GET /v1/facts items carry extractor, the model the extraction call reported (for example gpt-4.1-2025-04-14), on facts extracted from 0.10.4 on.
  • Events list their facts. GET /v1/events items carry derives: the live facts that cite the event. An event without facts has no derives key, as before.
  • The extraction model shown is the one running. GET /v1/admin/layers/stats reported the model the Settings page had just saved, before the restart that loads it. It now reports the model the running router was built with, and the saved one as pending_restart until the restart; the re-derivation cost estimate prices the running model too.

Erasure: receipts and proof

POST /v1/erasures hard-deletes a scope's events (or the named ones) and everything derived from them; docs/ERASURE_COVERAGE.md lists every store it clears. 0.10.4 makes the result provable:

  • The gdpr audit row. The execute writes one row with capability: forget.gdpr, the target scope, event_id = the erasure id, gdpr: true and the request's request_id (the X-Request-Id the response echoes); decision: allow with status 202, or failed with 502. Its id is the 202's audit_id (and details.audit_id on a 502); a replay of the same idempotency_key answers the same audit_id. GET /v1/audit now lists newest first, so the row tops the console's Audit page.
  • The receipt. Every job that ends is stored under erasure_job/<erasure_id> in the v1_layers store's default column family (no new column family), so GET /v1/erasures/{id}, /verify and /receipt answer after a restart. The receipt holds the erased scope path, the erased event ids (deleted and redacted apart, with a digest over all of them), the counts, the selection cutoff, timestamps, the requester, the audit row and request ids, the operator's audit_note (verbatim, at most 1,000 characters: put a ticket number in it, not personal data), digests of the canonical request and of the idempotency key, the authorization footprint, the residual clear time and the list of what the erasure keeps by design. Never content. GET /v1/erasures/{id}/receipt serves {"receipt": {...}, "sha256": "...", "durable": true}, sha256 over exactly the served receipt bytes.
  • The proof. GET /v1/erasures/{id}/verify (trusted operator: the static operator key, or an auth-disabled server) re-reads every store for what the receipt lists and answers one check per store (store, what, remaining, ok): the v1 log rows, idempotency records, content rows, vectors and the deferred-vector queue, BM25 documents, the WAL twins (no payload, no labels, no intent), chunk and session fragments, facts, beliefs, episodes and concepts citing the events, the no-resurrection markers, the scope tombstone, and, with CORTEX_COHORT=on, the cohort projections of the scope's ancestors and the erased customer's alias (a profile answers 404). checks_ok is every check passing; all_ok adds that log_residual_cleared_at is set. A check that cannot run answers ok: false with a note, and so does a check that did not reach every row (the whole-scope captured_wal scan stops after 1,000,000 rows). Two checks appear only when the proof cannot pass: entity_names, when the erased events had derived rows (they were enriched) and CORTEX_ENTITY_GRAPH is off, because the entity names and aliases enrichment extracted from them stay in resolver/ and entity_vector/ (docs/ERASURE_COVERAGE.md: "graph off: not covered"); and receipt_ids, when the receipt's id lists were cut at 250,000 ids of a kind. A customer that was never enriched, or a server with the graph on, proves clean as before. On a test copy of the 300-customer store (graph off): a never-enriched customer (321 events, one of them a byte canary) proved clean, 13 of 13 checks, all_ok, its text and label in no file; a customer enriched just before (229 events, 20 facts) answered all_ok: false on the entity_names check alone, and its receipt listed the extracted names among what it keeps. Measured on a synthetic three-customer store (crates/cortex-api/tests/erasure_proof.rs): every check passes after a whole-scope erasure, a label canary is in no unified_wal/ file once the residual cleared, and a planted content row and idempotency record are both reported.
  • No labels or intent left behind. The redacted WAL twin of every erased event used to keep the capture's labels and intent, and a redacted descendant row its labels, intent and location; both redactions now remove them. A copy an older binary redacted is redacted again when the erasure is re-run.
  • Counts drop at once. The exact lane and the read-all synthesis already counted from the projection; the ranked lane's subjects_authorized now skips a customer with no visible event.
  • The lifecycle link works. The 202's lifecycle_stream delivers that erasure's erasure_complete row and nothing else.
  • A realistic estimate. The preview's estimated_duration_ms is 1 s plus 2 ms per in-scope event (measured since DB-4: 0.56 s for 510 events, 2.8 s for 2,531).

What stays by design, and is listed on every receipt: the receipt itself, the audit rows, a whole-scope tombstone (the scope path and cutoff), the no-resurrection markers (event ids), the redacted copies' identities (ids, scope, timestamps), the scope registry entry until DELETE /v1/scopes, the deleted idempotency records' bytes until RocksDB compacts v1_idempotency/ (each write's idempotency key and scope path), copies outside the server (backups, the source system: a re-sync from it re-creates the customer, model provider logs of earlier answers), and, on a receipt whose events had been enriched while CORTEX_ENTITY_GRAPH was off, the entity names and aliases extracted from them.

In the admin console

The embedded console uses the new fields. Every page and switch that existed in 0.10.3 sends the same requests and renders the same way while the new switches are off (pinned by the web_ui tests: the leaf Ask, Compose and Recall code is hashed against the previous candidate, and a Recall record renders without the new provenance links).

  • Console › Ask, across customers. A "Where the time went" bar splits the round trip into CortexDB (plan and query), the model and the network (round trip minus the server's total), from Server-Timing; the phase chart is open and labelled, a wait for a warming projection is shown apart, and the cost is shown. A read-all answer shows its read block (map model, conversations read, unread, cut and dropped, calls, tokens, time) and its themes with their customer counts and example events. No model answers with skip_answer_llm; Re-run this plan without the model replays the answer's own plan and shows CortexDB's milliseconds.
  • Console › Recall, across customers. One ranked recall across the customers under a parent (subtree, diagnostics: "full"): the customers it reached, the events, and the search's stage times (counts are listed apart, never charted as times).
  • Presentation mode (Console page, per browser, off by default): views across customers and the Re-derive and Erasure pages show a customer by alias or as org:x/user:•••, and so do the scope rows of Memories, Observability › Usage, Data & scopes and Audit; the top-bar scope field is masked. Single-customer answers and event text still show what is stored.
  • Memories shows each fact's extraction model (extractor) and source events, and each event's derived facts; a click opens the event or the fact with its links.
  • Re-derive (Memory section): the running and pending extraction model and the enrichment scope allowlist; one scope's events, a dry run (plan and cost estimate), apply, the job's progress, and before and after facts per source event with the log proof (captured head unchanged, derived rows appended). The "before" facts are kept in this browser only (the JSON download is the durable copy). A model-only Save and restart control swaps the extraction model.
  • Erasure (Operate section): preview, type the exact scope to confirm, execute, wait for the residual clear, run /verify and show every check, download the receipt with its SHA-256 recomputed in the browser, and exact cross-customer counts before and after.
  • Audit lists newest first, with a scope column, a gdpr filter and a link from an erasure to its row.
  • Event cards (Memories, Recall, Ask evidence) show a small language badge when the event carries a non-English language or a second script: hi-Latn for Hinglish, Devanagari for und-Deva. The detail rows add language. An implicit-English event stores no language and renders exactly as before.

Fixes

  • Handler errors replaced the client's request id with a freshly minted one in the body and in X-Request-Id, so a client could not find its own request in the server's log. Every error envelope now carries the request's own id.
  • /v1/recall 422s did not say which field was wrong. They carry details.part, details.reason and details.field_path.
  • scripts/cold_backup.py named the wrong group (1000:1000) for the image's cortex user. It is 1000:999.
  • Parent-scope packs presented a storage-order sample as if it were the answer. They now say so (the note above), and the cohort lanes answer the questions the sample could not.
  • GET /v1/audit showed the oldest rows. It truncated an oldest-first list, so on a busy server (the console polls every few seconds) a recent row, an erasure's included, never appeared. It now lists newest first and limit keeps the newest rows.
  • POST /v1/erasures wrote no gdpr audit row (only the authentication layer's generic row, with no scope) and dropped audit_note. It writes one, returns its id, and keeps the note on the receipt.
  • An erasure job vanished at a restart (GET /v1/erasures/{id} answered 404). The job is kept as a durable receipt.
  • The redacted copies of an erased event kept its labels and intent, so a byte grep for a booking or vehicle id still found it in unified_wal/ after log_residual_cleared_at was set. Both redactions now remove them; re-running an erasure re-redacts copies an older binary left.
  • The erasure's lifecycle_stream link carried every lifecycle event (the erasure_id filter was ignored, and the erasure emitted none). It now delivers that erasure's erasure_complete row only, also when opened after the 202.
  • The erasure preview's duration estimate was about 30 times too high (the pre-DB-4 cost of 80 ms per event). It is now 1 s plus 2 ms per event.
  • The ranked lane's subjects_authorized counted customers whose events were all erased or forgotten, and could tag and alias them. It now probes each authorized customer for a visible event (up to 5,000 customers).
  • A small burst of enrichment jobs ran one after another. The view builder groups jobs into a batch and ran a batch's jobs in turn, so re-deriving four long conversations took the sum of their extraction times (about 2.5 minutes for four 12-18 KB transcripts). A batch now also runs its jobs on job permits that are free at that moment; CORTEX_ENRICHMENT_CONCURRENCY still bounds the jobs in flight, and under a backlog that holds every permit nothing changes.
  • Console: Settings › Save cleared a stored API key left blank. The key field never shows the stored key, and the server reads an empty value as "remove it", so saving a connection card to change its model also removed its key at the next restart. A blank key field is no longer sent.
  • Console: Re-derive's link to the facts opened the wrong scope (the top-bar working scope, not the customer just re-derived). It now makes that customer the working scope and opens Memories on its Facts tab (#/memories?layer=facts).
  • Console: Re-derive's before and after counted live facts only while the plan and the purge count the superseded versions too (a plan of 124 facts against "90 facts before"). Both sides now count every fact the events support, as live and superseded.
  • Console: the re-derivation estimate was called "a planning upper bound". It is a planning figure from fixed per-event constants: four long non-English transcripts on gpt-4.1 cost $0.24 against an estimate of $0.05. The page says so and points at Observability › Usage.
  • The cohort rules planner split Devanagari words at the virama and nukta (the refusal read 'ग' matches no intent for ग्राहकों), and read a word of a caseless script as an all-capitals proper noun. Words keep their combining marks, a Devanagari glossary phrase matches, and a Devanagari question the rules cannot map is refused unknown_term with its whole words (or planned by the model under CORTEX_COHORT_PLANNER=auto). Latin-script questions tokenize as before.
  • "Latest" and "recent" questions in Hindi and Hinglish did not infer order: "recency" (the cue words were English only), so on a typed event stream they were answered from relevance-ranked events and could name an older item as the newest. With CORTEX_RECENCY_CUES_MULTILINGUAL=1 (off by default) the answer route also reads हाल ही में, हालिया, नवीनतम, ताज़ा, सबसे नया, आखिरी, aakhri, haal hi mein, and last in a question detected as Hinglish. A "last" word is not a cue before a time unit (आखिरी बार, last week, as English last is not), as "in the end" (last mein, आखिरी में), or before a date or a price noun (aakhri tareekh, last date, आखिरी कीमत: a deadline or a final price is a stated fact). Every added word is one an English question does not use, so English inference is unchanged with the switch on or off (pinned against a frozen copy of the previous gates over the repo's English question files), and with it off a non-English leaf answer is byte-identical to 0.10.3's (pinned in the English regression gate). An inferred order still serves the newest-events tail outside holistic only on a typed event stream.
  • A non-English cross-customer question could drop a term the deployment had settled. Under CORTEX_COHORT_PLANNER=auto (or llm) the JSON-plan call saw the vocabulary but neither the operator glossary nor the words the rules had already mapped, because the rules return no notes for a question they cannot plan. A negation asked in Hinglish was planned with one step of an event kind where the same English question excludes every event of that kind (the rules map the word to its label family), so it listed more customers than the English question. The planner prompt now carries the operator glossary (phrase and facet) and, for a question the rules could not plan, the term mappings they resolved before they stopped, and tells the model to keep those meanings in any language. The same prompt now says that the ranked lane reads the conversations, in any language: with the extra sections the model had started to refuse "which customers discussed X on a call" as needs_conversation_reading. A reply with a sentence around its JSON object is read as the object (it used to be unusable), and when an unusable reply leaves a question refused, cohort.notes says so (LLM planner unusable (...)). A question the rules plan never makes the call, so English plans are unchanged.
  • A fully read long event could be dead-lettered as input_coverage_limit. With CORTEX_ENRICHMENT_LONG_TEXT_WINDOWING on, knowledge and entity extraction run once per window, before any retry or verifier polarity re-check, so a two-window non-English call re-derived with gpt-4.1 could spend the default budget of 8 attempts, skip its re-checks and count as failed (3 of 4 events in one rehearsal run). Unset, CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT now defaults to four attempts per window, at least 16, when windowing is on; without windowing it stays 8, and an explicit value wins.
  • Console: pasting the API key reset the Re-derive and Erasure forms. Any redraw of the page (pasting the key after a restart, a top-bar change) refilled the scope field with the last listed or previewed scope and set Re-derive's Modality back to "any", so a List events right after it picked every event of the customer (158 instead of its 4 conversations in one rehearsal run) or another customer; in Presentation mode the scope field is masked, so the swap could not be seen. What is typed or picked there now survives a redraw until it is listed or previewed.
  • The console missed a fast restart. A server that came back within one 1.5 s poll was never noticed: no reload, and a false "didn't come back" message after 2 minutes. The console now also compares the server's uptime with the time since it asked for the restart.

Measurements

The figures come from this release's code on a test copy of a 300-customer event-stream store, with the cohort flags on and the static operator key, graded against ground truth computed from the store's source data and frozen before the runs (version 2). The exact-lane and attribute figures are from the last full pass of the logic as released (2026-10-02); the read-all figures were measured again after the last change to its prompts (below). No question, customer or theme of that store is named here.

A cross-customer question set

30 questions (31 items with an anchored variant of one), graded on the structured result (cohort.result), never on the prose:

Questions/v1/cohort/query with a plan/v1/cohort/query with the question/v1/answer with cohort
19 exact-lane questions: lookups, counts, top-N, a funnel, segments, a sequence, an empty set, a relative window19/1919/1919/19
6 attribute questions answered from labels added at ingest6/66/66/6
6 questions about what customers said: 1 content lookup, 5 conversation syntheses, each reading every conversation of its cohortn/an/a (this route is the exact lane; it answers them unsupported)1/6

0.10.3 answered none of the 31 (its parent answers came from the storage-order sample).

  • One business term, settled in the glossary. "booked" had two readings in that store: any booking event, or only an initiated or confirmed one. Its owner ruled that it means any booking event, the store's cohort_glossary.json maps it so, and with that mapping the attribute questions pass 6 of 6 (5 of 6 under the other reading). Three questions (two exact-lane, one attribute) depend on a glossary entry for a business phrase: without it they answer unknown_term.
  • Narration check. On 23 of the 25 exact-lane and attribute answers every narrated number verified. On the other 2 the check could not tie one number to the block (a count written as the word "two", and the event count of a group tied at the cut-off) and replaced the narration with the server's own summary, as designed; the structured result was right on all 25 (an earlier candidate verified 25 of 25).
  • Time and cost. The 25 exact-lane and attribute answers took 2.5-7.0 s each (p50 3.6 s) at $0.010-0.028 each, of which CortexDB's own time is about 50-75 ms; with the six read-all items a full /v1/answer pass costs about $8.4 in model calls ($7.93 of it the read-all items).
  • The excerpt-sample lanes (read: "sample", or read-all off), measured on an earlier candidate's pass of those lanes: the lookup states that it is a sample and, over the customers its narration cites, reaches F1 0.59 (precision 0.89, recall 0.44; over every customer whose excerpt it read, F1 0.73, recall 1.0; bar 0.8; 0.54-0.67 across runs). The syntheses state their coverage on all five and find 1-4 of the ground truth's top-5 themes (bar: 4 of 5 on 4 of 5 questions), and each cites some excerpts that do not carry the theme they are cited for; the limit is how much of each conversation fits in an 8,000-token context. That pass cost $0.87-0.90 for all 31 answers.

Reading every conversation (lane: read_all)

Map model claude-haiku-4-5, check and narration calls on claude-opus-4-8, CORTEX_COHORT_SYNTH_PARALLELISM=8, _BATCH_TOKENS=40000, _BATCH_MAX=20, _MAX_CALLS=64, the same strict grader:

Content lookupSyntheses (5)
Conversations read52 of 5252 to 626 of 52 to 626, every cohort equal to the ground truth's
Customers listed12: precision 1.00, recall 0.67, F1 0.80 (bar 0.8: met)n/a
Ground-truth top-5 themes foundn/a3, 2, 3, 1, 4 (bar: 4 on 4 of 5 questions: not met)
Every narrated theme backed by 2 or more customersn/ayes, all five
Cited example conversations that carry the theme they are cited forn/a27 of 42 (64 %; per question 12/12, 8/10, 0/6, 0/2, 7/12; bar: 100 %: not met)
End to end12.7 s16-48 s
Model calls24 map, 1 narration8-33 map, 1 theme, 6 check, 1 narration
Cost per question$0.18$0.89-2.11 (0.30-1.49 M input tokens)

1 of the 6 items passes the strict grade (the lookup).

Measured again after a blind-rule fix. The worked examples in the read-all map, theme and check prompts had been written while iterating against this question set, and some of them paraphrased its questions and its list of themes. They were replaced with examples from unrelated domains (a bookshop, a phone app, a book club) or removed, and the six items were measured again: the figures above. With the earlier prompts the same items scored F1 0.81, top-5 themes 4, 2, 3, 1 and 4, and 33 of 42 valid examples (79 %); those were development-set figures for prompts tuned on the same questions, and are not claimed. What is left is agreement between two readers: the ground truth was labelled by a different model against a fixed list of themes, and on some questions the two name different reasons or aspects for the same conversation; which themes reach the top five also varies from run to run. Map models compared on the lookup and two syntheses (earlier prompts): claude-haiku-4-5 found the most of the lookup's customers (F1 0.77, against 0.62 for claude-sonnet-5 and the answer model, which read "discussed X" more narrowly) at the lowest cost ($2.14 for the three questions, against $2.92 and $5.55), with the same pass rate on the syntheses; it is the setting measured above. The development passes cost $44.83 in model calls on the test store, and the measurement after the fix $7.93.

Single-customer scopes do not change

  • The English regression gate (Golden A and B packs, and a leaf /v1/answer) is byte-identical under all 64 combinations of CORTEX_COHORT, CORTEX_SUBTREE_RECALL, CORTEX_PARENT_SAMPLE_WARNING, CORTEX_ENRICHMENT_SCOPE_PREFIXES, CORTEX_COHORT_WARM_ROOTS and CORTEX_COHORT_PLANNER=auto (the last three applied at boot, as the binary does), and no leaf response carries a Server-Timing header.
  • The 115-question customer-journey regression set, replayed without the answer model (skip_answer_llm) against three 0.10.3 replays, in three configurations (every new flag off; the cohort lanes, read-all and CORTEX_COHORT_PLANNER=auto on; those plus CORTEX_RECENCY_CUES_MULTILINGUAL=1), on the release candidate image and on a measurement build of the same code (nine runs): 112 to 114 of 115 answer contexts are byte-identical per run. Every difference is one of three questions that also vary between boots or builds of 0.10.3 itself: one whose ranked cut-off changes on every boot (different in all nine runs), one that swaps one event at a ranked cut-off (four runs) and one whose two events come in the other order with the same bytes (one run). Every run had 0 errors, 0 degraded sources and the newest-events tail on the same 83 questions, and no response carries a cohort block.
  • A non-English leaf /v1/answer whose only recency cue is a Hindi or Hinglish word is byte-identical, with CORTEX_RECENCY_CUES_MULTILINGUAL unset, to the same answer with order inference switched off, with and without view: "holistic" (the regression gate's own pin); with the switch on the same questions take the inferred order.
  • The multilingual mock evaluation: no top-5 change over 1,214 baseline queries.
  • LongMemEval was not re-run for this release: its scopes have no children, so no request reaches the new code.

Questions in Hindi and Hinglish

Measured on a synthetic store shaped like the customer-journey data (12 customers under one parent, typed event streams of about 35 events each, four of them bulked to about 435, one conversation each in English, Hinglish or Hindi script), answer model claude-opus-4-8, read-all map model claude-haiku-4-5, the same store and questions before and after. "After" is the multilingual recency words on (CORTEX_RECENCY_CUES_MULTILINGUAL=1, measured before the switch existed, when they were always read; off by default in this release) and CORTEX_COHORT_PLANNER=auto:

Question setBeforeAfter
"Latest" / "recent" questions on one customer in Hindi and Hinglish without an English cue word (5)0 of 5 named the newest item5 of 5
"Latest call / chat" questions in Hindi and Hinglish (4)3 of 44 of 4
The same shapes asked with an English cue word (4)4 of 44 of 4, the same inference
Across customers, CORTEX_COHORT_PLANNER=auto: a count, a list, a read-all lookup and themes in Hinglish and in Hindi (8)7 refused unknown_term, 1 planned as a lookup of "karte hain"8 of 8, the same customers as the English question
The 4 English cross-customer questions under autothe same plan, count and customers as rules, no planner call

A newest-slice answer now reads the newest-events tail, about 10,900 input tokens instead of about 7,300 (about $0.018 more per answer at the opus rate); the median answer time stayed at about 2.5 s. The planner call adds about 1-5 s and about $0.01 to a non-English cross-customer question ($0.018-0.024 for an exact one, $0.033-0.086 with read-all).

Other checks

On the release commit's code: the workspace test suite (8,374 tests passed, 0 failed, 209 test binaries), the English regression gate with its multilingual-cue pin, the erasure proof suite, the script gates, and the multilingual mock evaluation (no top-5 change over 1,214 baseline queries; it measures /v1/recall, which CORTEX_RECENCY_CUES_MULTILINGUAL does not touch). On the previous candidate, whose code differs only in the erasure proof's checks, the cohort answer's deadlines, the read-all prompts and that default-off switch: the live Docker suite (43 of 43 and every scenario), the upgrade and rollback probe (85 of 85, "Rollback"), the server, tenant-isolation, backup and restore, retention, WAL-migration and commitment smokes, and the Python SDK, CLI, MCP and TypeScript client suites.

Environment variables

All new, all read from the process environment only (docs/generated/env-vars.md §9.2 has the full rows):

VariableDefaultWhat it does
CORTEX_COHORToffon: the exact lane, /v1/cohort/* and /v1/answer cohort.
CORTEX_SUBTREE_RECALLoffrequest: /v1/recall subtree and the ranked and synthesis lanes of /v1/answer cohort.
CORTEX_COHORT_PLANNERrulesllm adds one JSON-plan call per cohort answer; auto (recommended for multilingual users) makes it only for a question the rules cannot plan or that is not English.
CORTEX_COHORT_MAX_EVENTS2,000,000Events under one scope; past it 413.
CORTEX_COHORT_MAX_BYTES1 GiBMemory of every resident projection together; least recently used evicted, 413 for one alone past it.
CORTEX_COHORT_CACHE_ROOTS4Resident projections, and the depth of the build queue.
CORTEX_COHORT_WAIT_MS3,000Wait for a build or repair before 503 COHORT_WARMING.
CORTEX_COHORT_WARM_ROOTSunset (nothing warmed)Comma-separated scopes whose projections are queued for a background build at boot.
CORTEX_COHORT_DEADLINE_MS5,000Reserved for a large-scope tier that is not built.
CORTEX_COHORT_SYNTH_MAX_CALLS32Read-all synthesis: the most map calls one cohort answer makes to read every conversation of its cohort (at most 256); past it the newest are read and the answer is partial. 0: the ranked sample instead.
CORTEX_COHORT_SYNTH_BATCH_TOKENS60,000Read-all: estimated conversation tokens per map call.
CORTEX_COHORT_SYNTH_BATCH_MAX40Read-all: conversations per map call.
CORTEX_COHORT_SYNTH_PARALLELISM6Read-all: map calls in flight per answer (at most 8); each also takes a permit of the shared provider lane.
CORTEX_COHORT_SYNTH_DEADLINE_MS45,000Read-all: budget of the map and theme calls (at most 60,000), ending at least 35 s before the answer budget; unfinished calls leave their conversations unread (partial).
CORTEX_COHORT_ANSWER_BUDGET_MS85,000The whole cohort /v1/answer from the request's start (40,000-95,000): planner call (at most 15 s), reads, map, theme and check calls and narration all finish inside it; a narration that misses it answers with the server summary.
CORTEX_COHORT_SYNTH_CONVERSATION_MAX_BYTES48,000Read-all: bytes of one conversation a map call reads; a longer one keeps its head and tail and is counted.
CORTEX_COHORT_SYNTH_MAP_MODELthe answer modelRead-all: the model of the map and theme calls, on the answer provider and key.
CORTEX_SUBTREE_MAX_INFLIGHT2Concurrent ranked passes; the excess waits to the deadline, then 503 SUBTREE_BUSY.
CORTEX_SUBTREE_DEADLINE_MS2,000Deadline of one ranked pass, checked inside its scan.
CORTEX_SUBTREE_EXACT_VECTOR_MAX1,000,000Vectors in the authorized subtree, counted before any scan; past it the ranked lane is refused (subtree_too_large).
CORTEX_PARENT_SAMPLE_WARNINGonThe parent-scope note; 0 / off / false / no turns it off.
CORTEX_ENRICHMENT_SCOPE_PREFIXESunset (every scope)Enrichment only for captures under these comma-separated scopes; set but naming no valid scope = nothing is enriched (§4.2).
CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT8; with long-text windowing on, 4 per window, at least 16Not new: only its default with CORTEX_ENRICHMENT_LONG_TEXT_WINDOWING on changed (Fixes).
CORTEX_RECENCY_CUES_MULTILINGUALoff1: /v1/answer's inferred newest-first order also reads Hindi and Hinglish "latest" / "last" words (Fixes). Off: non-English answers as in 0.10.3.

Error codes

HTTPCodeWhereRetriable
409COHORT_DISABLED/v1/cohort/*, /v1/admin/cohort/warm, /v1/answer with cohort, while CORTEX_COHORT is offno
409SUBTREE_RECALL_DISABLED/v1/recall with subtree, a ranked plan, while CORTEX_SUBTREE_RECALL is offno
404COHORT_ALIAS_NOT_FOUNDA focus alias that names no customer the caller may readno
413COHORT_SUBTREE_TOO_LARGEPast CORTEX_COHORT_MAX_EVENTS or CORTEX_COHORT_MAX_BYTESno
422COHORT_NO_DESCENDANTSNo authorized customer scope under the scope holds datano
503COHORT_WARMINGThe projection is building or repairing, or the build queue is full (Retry-After)yes
503COHORT_STATE_CHANGEDPOST /v1/cohort/query with a cursor whose last customer is no longer listed with the same count: restart the listingyes
503COHORT_UNAVAILABLEA read the cohort lane needs failedyes
503SUBTREE_BUSYThe ranked lane's in-flight cap or deadline (Retry-After)yes
503COHORT_REQUIRES_UNIFIED_WALA cohort request on a legacy-WAL serverno
422ENRICHMENT_SCOPE_NOT_ALLOWEDPOST /v1/admin/derived/rederive re-enriching an event outside CORTEX_ENRICHMENT_SCOPE_PREFIXES (dry run too; nothing changed)no
409ERASURE_NOT_COMPLETEDGET /v1/erasures/{id}/verify on a running or failed jobyes, once it completes
409ERASURE_RECEIPT_UNAVAILABLE/verify or /receipt on a job with no receipt (still running, or run by an older binary)no

New strings, not error codes: the pack warnings parent_pack_unranked_sample: ... and subtree_recall_unavailable: <reason>, and the /v1/answer diagnostics.degraded_sources codes subtree_recall_unavailable, cohort_narration_unverified, cohort_narration_failed and cohort_synthesis_partial (a read-all answer that did not read every conversation of its cohort).

Files in the data dir

Created only when a cohort lane is used; 0.10.3 ignores both:

  • cohort_alias.key: 32 random bytes, mode 0600, created on the first cohort request; the key of the stable aliases. Back it up with the data dir: deleting or replacing it changes every alias a client may have stored.
  • cohort_glossary.json (optional, written by you): business phrases mapped to facets.

Written by every POST /v1/erasures job that ends (not a file: keys in an existing store):

  • erasure_job/<erasure_id> in the v1_layers store's default column family: the job's receipt (see "Erasure: receipts and proof"). 0.10.3 never reads the prefix.

Startup warnings

The startup configuration lint lists each new variable with its effective value. With CORTEX_ENRICHMENT_SCOPE_PREFIXES set it also names what the allowlist does not confine: ENRICHMENT_SCOPE_ALLOWLIST_LAYERS_AUTO (CORTEX_V1_LAYERS_AUTO on), ENRICHMENT_SCOPE_ALLOWLIST_COMPACTION (CORTEX_CONSOLIDATION_ENABLED opted in), ENRICHMENT_SCOPE_ALLOWLIST_INERT (enrichment off) and ENRICHMENT_SCOPE_PREFIX_INVALID.

Rollback

A 0.10.3 image opens a data dir 0.10.4 has used, with the cohort flags on or off: no column family, directory, WAL row kind or index field is added (the one new key prefix, erasure_job/, holds erasure receipts that 0.10.3 never reads: after a rollback GET /v1/erasures/{id} answers 404 for jobs 0.10.4 ran, as for any pre-restart job on 0.10.3), and the cohort projection is in memory only. To roll back, stop 0.10.4 with the usual 120 s grace and start 0.10.3 on the same data dir; to turn the lanes off without a rollback, unset the flags and restart. A fact extracted by 0.10.4 carries one more metadata key (extractor_model), which 0.10.3 keeps and ignores; 0.10.3 does not read CORTEX_ENRICHMENT_SCOPE_PREFIXES, so turn enrichment off in its environment (unset CORTEX_ENRICHMENT_DELAY_SECONDS) before rolling back a store that relied on the allowlist: 0.10.3 would otherwise enrich every scope.

What was checked, on one data dir, with a graceful stop between each step:

  1. 0.10.3 wrote captures under several customer scopes.
  2. 0.10.4 (flags on) read them, wrote more, built a projection and answered exact, joined (via) and ranked cohort questions, checked the vocabulary, and confirmed that a leaf recall has no subtree.
  3. 0.10.3 again opened the store and served everything; its /v1/cohort/query answered 404, and a customer forget succeeded.
  4. 0.10.4 again rebuilt the projection without the forgotten customer.

85 of 85 checks passed, with no error line and no panic; the only new top-level entry was cohort_alias.key.

Known issues

New in 0.10.4:

  • Read-all answers are the model's reading of each conversation. The server counts customers and conversations exactly over those readings, but whether a conversation "shows" a topic, and which theme it belongs to, is the map model's judgement. Read-all sends every conversation of the cohort, in full up to CORTEX_COHORT_SYNTH_CONVERSATION_MAX_BYTES, to the model provider (with no scope, alias or event id); its spend is per question, in the foreground, and grows with the cohort's conversation text (cohort.read.cost_usd states it). With the answer model as the map model a cohort of several hundred conversations can outrun the 45 s read budget and answer partial; set a faster CORTEX_COHORT_SYNTH_MAP_MODEL.
  • The ranked-sample lanes (read: "sample", or read-all off) are below their accuracy bar (see "Measurements"): they read a ranked sample of excerpts under an 8,000-token context, and their cited customers are the model's reading of that sample. Use them for exploration, and the exact lane for anything you count or list.
  • The idempotency store keeps an erased write's key bytes until RocksDB compacts it. An erasure deletes the idempotency records of the erased events (the idempotency check), but the raw-residual pass does not compact v1_idempotency/, so each deleted record's key (the client's idempotency_key and the write's scope path, no memory content) stays in that store's files until RocksDB's own compaction. The receipt lists it among what the erasure keeps.
  • A long event is read in one 12 KB window unless long-text windowing is on. Enrichment reads the first CORTEX_ENRICHMENT_LONG_TEXT_MAX_BYTES (12,000) of an event and, with CORTEX_ENRICHMENT_LONG_TEXT_WINDOWING unset, dead-letters a longer one as input_coverage_limit (its first window's facts are kept); a re-derivation then counts it failed. Where conversations run long, set CORTEX_ENRICHMENT_LONG_TEXT_WINDOWING=1 (up to 4 windows, 48 KB); the per-event attempt budget then defaults to 16 (CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT, see Fixes).
  • Business terms need the glossary. The rules planner maps only what the vocabulary or cohort_glossary.json spells. A term with two meanings in your business has to be settled there; the plan echo (cohort.notes) always shows which mapping was used.
  • The number check pairs by position inside a clause and accepts a count that equals another count of the block (see "Coverage and the number check"). It fails closed: in the final measurement pass 2 of the 25 exact-lane narrations (a number written as a word, a tied group's event count) were replaced by the server's own summary, with narration_unverified_numbers naming the number; the structured result was right on all 25.
  • Read-all syntheses are the model's reading. Their counts are complete over every conversation read, but which themes a question gets, and which conversation stands as each theme's example, is a reading by two models (map, then check) and varies between runs; on the test store about four in five cited examples matched an independent labelling. Read the themes as a summary, and use the exact lane for anything you count or list.
  • Text-only attributes cannot be counted. Label them at ingest.
  • Scopes above 2,000,000 events (CORTEX_COHORT_MAX_EVENTS) and subtrees above 1,000,000 vectors (CORTEX_SUBTREE_EXACT_VECTOR_MAX) are refused; the large-scope tier is not built.
  • A synthesis over a cohort of more than 50,000 events answers 413 instead of reading a truncated sample.
  • cohort_alias.key is created without a temporary file. A process killed between creating and writing it leaves an empty file, and every cohort answer then answers 503 COHORT_UNAVAILABLE (marked retriable) until the empty file is deleted. After replacing the file, restart the server so every lane reads the new key.
  • cohort.focus resolves stable aliases only: an alias from a naming: "alias_ephemeral" answer answers 404 COHORT_ALIAS_NOT_FOUND.
  • The parent note's M (parent_pack_unranked_sample: events from K of M) can count a scope whose events were all erased or forgotten (it reads the scope-partition map). The ranked lane's subjects_authorized no longer does, up to 5,000 customers; past that it counts every authorized customer, as before.
  • Erasure receipts have no delete route. A receipt names the erased scope and event ids (identifiers, not content) and is kept for the life of the data dir, like the audit trail.
  • /verify reads what the receipt lists. A receipt keeps at most 250,000 event ids of each kind (event_ids_truncated past that); such a proof adds a failed receipt_ids check and is never all_ok, although the scope-level checks still cover a whole-scope erasure's remainder.
  • An enriched customer's extracted names survive its erasure while CORTEX_ENTITY_GRAPH is off. Enrichment registers the entity names and aliases it extracts in resolver/ (and their vectors in entity_vector/) with or without the graph, and an erasure finds them only through the graph (boot warning ENRICHMENT_RESOLVER_ERASURE_RESIDUE). The proof says so: such an erasure's /verify carries a failed entity_names check and its receipt lists the names among what it keeps. A customer that was never enriched proves clean.
  • Server-Timing is not in the CORS Access-Control-Expose-Headers list (adding it would change every cross-origin response), so a page on another origin cannot read it; the admin console is same-origin, and a non-browser client reads it as any header. A proxy in front of the server may add metrics of its own: read the header by metric name.
  • A read-all answer's per-call map time (cohort.read_all.map.call_p50 / call_max) includes the wait for a provider-lane permit, so under concurrent answers it measures queueing as well as the model.
  • cohort.as_of shows process-wide visibility counters, which move with every tenant's forgets.
  • With CORTEX_COHORT_PLANNER=llm, a failed planner call puts the provider's error text in cohort.notes; the rules plan is used.
  • A POST /v1/cohort/query cursor is not bound to the request body: reuse it only with the same body.
  • Building a projection while a forget at or above the scope runs keeps the scope warming (503 COHORT_WARMING) until the forget finishes.
  • Non-English questions across customers need CORTEX_COHORT_PLANNER=auto (or llm). The rules planner's vocabulary, operator words and glossary are English: under the default rules a Hinglish or Hindi cross-customer question is refused unknown_term (no model call, no cost). Set auto where users ask in more than one language; English questions the rules plan are answered exactly as under rules.
  • Spanish text is not detected as Spanish: an untagged Spanish capture is stored as untagged English (no language, no gloss).
  • Not yet run on a copy of a real store: the design's safety probes (a canary tenant, a signed token whose roster excludes some customers, a marker-only partial erasure). The same properties are covered by tests on synthetic stores.

Carried over from 0.10.3: see "Known issues" in RELEASE_NOTES_0.10.3.md.

Clients

No client changes. Every client released with 0.10.3 (Python SDK cortexdbai 0.12.0, TypeScript SDK cortexdbai 0.10.1, CLI cortexdb-cli 0.6.0, MCP cortexdb-mcp 0.8.0, connectors 0.2.22) works against 0.10.4 unchanged. None of them sends cohort or subtree yet; call the routes and fields directly (they are plain JSON). What existing clients can see:

  • at parent scopes, the parent_pack_unranked_sample note in a recall pack's warnings[] (the MCP server's memory_search does not surface pack warnings, so an agent using it does not see the note);
  • an error envelope's request_id equal to the X-Request-Id they sent;
  • /v1/recall 422s with details.part, details.reason and details.field_path.

Notes

Single-node remains the only supported topology.

Recommended for stores whose users ask in more than one language: CORTEX_COHORT_PLANNER=auto. The default stays rules, so an existing deployment plans every question as before until it opts in.

v0.10.3

`order=recency` honoured on leaf-scope answers, event-kind coverage, an answer cache, one-click updates, and a JSON error for every rejected request

0.10.3 is a fix release on top of 0.10.2, the last published release. Everything in RELEASE_NOTES_0.10.2.md still applies; this page lists only what changed since, including the work merged after 0.10.2 was cut (the answer cache, one-click updates, the directives.extract fix and the best-practices guide). It adds no column family, directory or on-disk format, and nothing is migrated: a 0.10.2 server opens and serves a data dir 0.10.3 has used (see "Rollback").

Full notesHide notes79 notes

Summary

  • order: "recency" is honoured where it was silently ignored. From v0.9.0 to 0.10.2 the newest-first timeline tail ran only in the holistic view. Every /v1/answer over an exact scope with no child scopes recalls granular, so an explicit order: "recency" there, and on /v1/recall in any other view, was served ranked. It now serves the tail in holistic, granular, lineage and raw. An order /v1/answer infers from the question ("latest", "currently", ...) takes the tail outside holistic only on a typed event stream (most of the newest events carry context.intent); conversation scopes stay ranked, as before.
  • Event streams keep their rare events in the answer context. On a scope whose events carry context.intent, the answer context keeps the newest event of each kind (an order cancellation 700 events behind a burst of page views), after a [note] Older history: line, and a question that names a kind gets that kind's newest event.
  • An over-budget timeline tail loses its oldest events, not its newest, and stays newest-first.
  • The premise guard no longer discards correct answers about a scope's own subject, and an abstention may now carry the absence report that led to it.
  • Measured on a 115-question customer-journey regression set (typed event streams, the customer's own request shape through the API, no view): 81 correct on 0.10.2, 114 on 0.10.3.
  • Answer cache (opt-in, CORTEX_ANSWER_CACHE=1): a repeated question returns the stored answer without recall or a model call, only while it is provably current.
  • One-click updates (opt-in): the admin console can install a release through an updater sidecar that backs up the data first and rolls back a failed update.
  • directives.extract is honoured on single captures, so "search only" captures stop running every enrichment stage.
  • Every request the server refuses before its handler runs gets the JSON error envelope (an unknown field, malformed JSON, a wrong Content-Type, a bad query string or path), with the status it always had and the request's own id.
  • No background LLM spend with enrichment off: the scheduler's compaction pass no longer calls the consolidation model when CORTEX_ENRICHMENT_DELAY_SECONDS is unset.
  • Operator fixes: failed requests' log lines carry error_code and retriable; uptime_seconds counts from process start; scripts/cold_backup.py refuses links, devices and escaping paths; the startup lint's RAM line is fixed.
  • One client change: the TypeScript SDK 0.10.1 adds the extract and answer_cache types. Every other client released with 0.10.1 works against 0.10.3 unchanged.

Upgrade from 0.10.2

Before you upgrade: what clients and operators will notice

  • /v1/answer callers that send no view on exact scopes with no child scopes (most direct API integrations; the SDKs and the CLI send view: "holistic" by default and are not affected by this item):
    • an explicit order: "recency" now serves the timeline tail, the scope's newest budgets.per_layer_limits.events events (default CORTEX_RECENCY_TAIL_EVENTS, 100), in place of ranked retrieval. Evidence older than those events is then not in the context, apart from the event-kind additions below;
    • a recency-shaped question ("recent", "latest", "newest", "currently", "next best action") without order takes the tail only when the scope is a typed event stream: more than half of its newest events carry context.intent. On every other scope (chat histories, documents) the answer stays ranked, exactly as in 0.10.2.
    • To keep 0.10.2's ranked behaviour on one request, send order: "relevance"; for the whole server, set CORTEX_RECENCY_INFERENCE=0 (inferred order) or CORTEX_RECENCY_ORDER=0 (the tail itself).
  • /v1/recall with order: "recency" and view granular, raw or lineage now returns the newest-K tail; it returned ranked events before. A raw tail fills layers.events only. descend and structured stay ranked.
  • A timeline tail that names none of the question's capitalised names (for example "Trek" in "how often do I ride the Trek?") is no longer served as an empty context: ranked retrieval runs instead. This applies to every view, holistic included.
  • Answers on typed event streams read more, and cost a little more. Where the tail now replaces ranked retrieval it fills the answer context up to budgets.max_tokens (default 4,000, at most 8,000), which a ranked context often did not. Event-kind coverage then costs one more bounded read of the scope's own events (at most 8 pages of 1,024) on each answer that uses it; its entries take the place of the oldest tail events, so the context never exceeds budgets.max_tokens. On the regression set above, an answer used about 7 % more input tokens (7,434 against 6,957) and 11 % more output tokens (291 against 261), and finished sooner (p50 4.9 s against 5.6 s). Scopes whose events carry no context.intent are unaffected.
  • The answer context of a typed event stream can end in [note] Older history: ... followed by the newest event of each kind the newest events do not include (CORTEX_ANSWER_KIND_COVERAGE=0 turns this off). Scopes whose events carry no context.intent are unchanged.
  • An abstention can carry a trailing absence report: "The information provided is not enough. The memories contain no record of the user viewing a Model X widget." Match an abstention by its leading "The information provided is not enough.", never by equality with that sentence.
  • Events, recall items and exports carry extract (the stages the capture's directives.extract allowed) when the capture set one. A client that validates events against a closed schema must accept the field. Events written before 0.10.3 carry none.
  • Servers with enrichment off stop background consolidation. CORTEX_CONSOLIDATION_ENABLED now defaults to following enrichment: with CORTEX_ENRICHMENT_DELAY_SECONDS unset, the scheduler's compaction pass runs no memory consolidation, knowledge synthesis or concept abstraction (no LLM call). Set CORTEX_CONSOLIDATION_ENABLED=1 to keep them. Each pass logs llm_stages (enabled, explicit or enrichment_unset).
  • Rejected requests answer JSON, not plain text. Statuses do not change: 422 INVALID_BODY for a body that does not fit the route (unknown field, wrong type, missing field), 400 INVALID_BODY for a body that is not JSON or could not be read, 415 UNSUPPORTED_MEDIA_TYPE, 400 INVALID_QUERY, 400 INVALID_PATH_PARAMS, and INVALID_REQUEST for any other plain-text 400 or 422. A client that parsed the plain text must read the envelope's message (it keeps the same words). The SDKs now raise these codes instead of UNKNOWN.
  • The request log line of a failed request carries error_code and retriable. A log pipeline that parses the line's fields sees two more on non-2xx lines.
  • /v1/admin/metrics uptime_seconds counts from process start, not from the first metered request.
  • scripts/cold_backup.py refuses more. backup exits 2 when the data dir holds a symbolic link or a special file (it names them); restore and verify exit 4 on an archive member that is a link, a device or a FIFO, or whose path is absolute or leaves the target, and on an archive with no manifest. Restored files now belong to the user running the restore (root under sudo), not to the owners recorded in the archive: chown the restored dir to the server's user before starting it (chown -R 1000:1000 for the image's cortex user; chown -R --reference=<old data dir> for a systemd unit). restore prints the owners the archive recorded.
  • Nothing changes unless you opt in for the answer cache (CORTEX_ANSWER_CACHE) and one-click updates (the docker-compose.updater.yml overlay).

Upgrade steps

Nothing is migrated and there is nothing to run afterwards. Replace the binary or image and restart as for any release, giving the server 120 s to stop. Then check:

  1. GET /v1/admin/version: crate_version 0.10.3 and the git_sha you deployed. The capability list is the same 11 names as 0.10.2.
  2. GET /v1/admin/ready: ready: true.
  3. With enrichment off and the background scheduler on, the compaction log line reports llm_stages="enrichment_unset"; set CORTEX_CONSOLIDATION_ENABLED=1 if you relied on consolidation.
  4. If your client calls /v1/answer without view on event-stream scopes and wants ranked answers, send order: "relevance".

Upgrading from 0.10.1 or v0.9.13

0.10.3 contains everything in 0.10.2. Read RELEASE_NOTES_0.10.2.md, "Upgrade from 0.10.1" (and from there RELEASE_NOTES_0.10.1.md for v0.9.13) first; this page applies on top.

What changed

Recall and answer: order: "recency" in every view that reads the scope's own events

order: "recency" replaces ranked retrieval for the events layer with the scope's newest eligible events by observed_at, newest first, with the same eligibility as ranked mode (scope, tenant, filters, time window, forgets, cancels). Since v0.9.0 it ran only in holistic, and /v1/answer recalls an exact scope with no child scopes granular, so both an explicit and an inferred order were dropped there without a trace. 0.10.3:

Recall viewExplicit order: "recency"Order inferred by /v1/answer
holistictail (as before)tail (as before)
granular (the leaf-scope answer view), lineagetail (was ranked)tail on a typed event stream, else ranked (as before)
raw (/v1/recall only)tail in layers.events, no context block (was ranked)n/a
descend, structuredrankedranked

A typed event stream is a scope where more than half of the tail's events carry a non-empty context.intent. The inferred tail is limited to them because a cue word says nothing about where a conversation's evidence is: in a chat history, "which bike am I currently riding?" is answered by a turn from weeks ago, not by the newest 100 turns (measured on LongMemEval below). A tail the entity grounding gate would clear (no event names the question's capitalised names) falls back to ranked retrieval instead of an empty context, in every view.

Answer: the over-budget cut keeps the newest events

The answer context is capped at budgets.max_tokens (default 4,000, at most 8,000). A timeline tail longer than that now keeps its newest-first order and loses its oldest events. Before, the cap re-ranked the tail by evidence type, could drop its newest event, and still told the model the context was newest-first.

Answer: event-kind coverage (CORTEX_ANSWER_KIND_COVERAGE, default on)

On a scope whose events carry context.intent, /v1/answer reads the scope's kind census: the newest eligible event of each distinct intent (at most 32 kinds; a scope with more gets none), under the recall's scope, view, filters and time window. The census is one bounded read of the scope's own events (at most 8 pages of 1,024; a scope whose events do not fit gets none). It is skipped for use_pack_id, for an answer that descended into child scopes and on native reads.

  • Over a timeline tail, the context keeps the newest events that fit and then, after [note] Older history: ..., the newest event of each kind those do not include. Each is older than every event above the note, so the context stays newest-first, and the whole stays within budgets.max_tokens.
  • Over a ranked context, when the question names a kind (a word of the intent: "order" names shop.order.cancel, unless every kind of the scope carries that word), the newest event of each named kind is added.

Events without an intent have no kind, so a scope without intents is unchanged.

Answer: premise guard

When an answer asserts something but also says the memories do not mention what the question presupposes, /v1/answer replaces it with "The information provided is not enough." Changes:

  • A note that the request scope's own identifier does not appear in the memory text ("no record of the user id <id>" over scope .../user:<id>) no longer triggers it: every recalled memory belongs to the scope.
  • When the answer opens with a plain absence report about the asked thing, that clause is kept after the abstention. The clause ends at the first comma, dash, semicolon or sentence end, and a leading "No" and everything after the clause are dropped, so the result is still an abstention, never a negative answer ("No, there is no record of who you went with, you went alone" keeps only "There is no record of who you went with").
  • A question that names the scope's own identifier gets a prompt note that the memories are that subject's history.

Answer cache (opt-in)

With CORTEX_ANSWER_CACHE=1, a repeated /v1/answer from the same caller and token, in the same scope and view, with the same answer-shaping fields (and the same UTC day when the request has no question_date), returns the stored response without recall or a model call, while it is provably current:

  • A write at the scope, below it or at a read ancestor (including one up to 5 s before the answer started), any forget, erasure, cancel or redact, or an ACL change invalidates it; a scope migration, a conflict resolution and a live feature toggle clear the cache.
  • A response is stored only when indexing was current and no evidence source was degraded, including an answer started while the scope-partition map was still building after a restart (scope_partition_map, below).
  • CORTEX_ANSWER_CACHE_TTL_SECS (default 300, at most 540), CORTEX_ANSWER_CACHE_MAX (default 1,000 entries). Held in memory only.
  • Cache-Control: no-cache answers now and replaces the stored answer; no-store neither reads nor writes. Streams, skip_answer_llm, use_pack_id and facet-plan overrides bypass it.
  • x-cortex-answer-cache: hit | miss | bypass on every 2xx while it is on; diagnostics.answer_cache (hit, age_ms) on a hit, where tokens_input and tokens_output read 0; an answer_cache block of counters on GET /v1/admin/metrics.
  • Inert on the legacy WAL (CORTEX_UNIFIED_WAL=0), with a startup warning.

One-click updates (opt-in)

docker-compose.updater.yml adds an updater sidecar, the only container with the Docker socket, with no published port. With CORTEX_UPDATER_URL and CORTEX_UPDATER_TOKEN set, the admin console's Updates page offers Update now: POST /internal/ui/update asks the sidecar to check the data is on a volume, pull the release while serving, take a cold backup of the data, stop the server gracefully, recreate it on the new image with the same settings, wait for health, and roll back on failure. It refuses an older version (422 DOWNGRADE_REFUSED) and answers 503 UPDATER_NOT_CONFIGURED without a sidecar. docs/PRODUCTION_DEPLOYMENT.md has the setup, and TELEMETRY.md describes the one new call (to the sidecar on the compose network; CortexDB makes no new outbound call).

directives.extract on single captures

A POST /v1/experience under the unified WAL lost directives.extract when the indexer rebuilt the request from the stored event, so a "search only" capture ran every enrichment stage. The directive is now stored on the event (extract, omitted when absent), honoured by the indexer, returned on events, recall items and /v1/export, and restored by /v1/import/jsonl. Events written before 0.10.3 carry none and are not repaired. Triples are always extract-none.

Recall: scope_partition_map degraded source

After a restart over a large store the server builds its scope-partition map in the background. Until it lands, descendant listings and cold-tier vector scans come back empty without an error. Recall now names a scope_partition_map: ... entry in diagnostics.degraded_sources (for a caller that may read diagnostics), /v1/answer reports the scope_partition_map code to every caller, and the answer cache never stores such an answer.

Errors: the envelope for every rejected request

A request whose body, query string or path parameter the route's extractor could not read was answered with a plain-text body. It now gets the error envelope (error_code, message, request_id, retriable: false, details.part, details.reason; /v1/answer and /v1/recall/stream add details.field_path), with the status unchanged (table below). The envelope's request_id and the X-Request-Id header are the request's own id: the client's when it sent one, else the one the server minted for the request, which is also the id of the request log line and of the authentication layer's audit row. The 413 of an oversized body does the same.

Operations

  • Request log: a line for a response outside 2xx whose body is the error envelope carries its error_code and retriable (never its message); both are absent on 2xx and non-envelope bodies.
  • Compaction LLM stages follow enrichment unless CORTEX_CONSOLIDATION_ENABLED says otherwise (see "Before you upgrade"). The deterministic stages (fact compaction, communities, procedures) are unchanged.
  • uptime_seconds on GET /v1/admin/metrics counts from process start.
  • Startup lint: on hosts at or below the RAM model's ~18 GiB baseline it says the model has no estimate, instead of "fits roughly 0M hot vectors"; counts print as 488k / 44.9M.
  • scripts/cold_backup.py: see "Before you upgrade". Extraction uses Python's tarfile data filter where it exists and otherwise writes the checked members itself; it never extracts fully trusted. A missing manifest is exit 4 instead of a traceback.

Docs

docs/BEST_PRACTICES.md (ingestion, embeddings, what runs on-prem, answer reuse, staying current, launch checklist), the embedding migration runbook and the production deployment guide are in the admin console's docs. The API reference documents all of the above, and docs/schemas/openapi.json adds order to /v1/recall, the answer cache headers and metrics, and the stored extract.

Fixes

  • order: "recency" was ignored on every leaf-scope /v1/answer and on /v1/recall outside holistic, explicit or inferred, with no diagnostic (since v0.9.0). It is now served in every view that reads the scope's own events (above).
  • An over-budget recency tail was re-ranked by evidence type by the answer packer and could lose its newest event. It is now cut oldest-first.
  • A recency tail with none of the question's names was cleared by the grounding gate and served as an empty context. Ranked retrieval now runs instead.
  • The premise guard turned correct answers into abstentions when the model remarked that the scope's own id does not appear in the memory text. The scope's own id no longer counts as a missing premise.
  • directives.extract was lost on single captures under the unified WAL, so they ran every enrichment stage. It is stored on the event and honoured.
  • Extractor rejections answered plain text (an unknown field, malformed JSON, a bad query string or path; a 0.10.2 known issue). They answer the JSON envelope with the request's own id.
  • The scheduler's compaction pass called the consolidation model with enrichment off. It makes no LLM call unless CORTEX_CONSOLIDATION_ENABLED=1.
  • uptime_seconds counted from the first metered request, and the startup lint printed "fits roughly 0M hot vectors" on hosts with less than ~18 GiB of RAM. Both are fixed.
  • scripts/cold_backup.py archived a symbolic link's target, silently skipped the contents of a symlinked directory, and on Python before 3.14 extracted archives fully trusted (links and devices included). Backup now refuses and names such entries; restore extracts checked members only.

Measurements

Measured against the published 0.10.2 image, one host per test. Unless a paragraph says otherwise, the figures come from the 0.10.3 release candidate before its review fixes; the last section checks the final build against it.

A customer-journey regression set

115 questions over typed event streams (page views, transactions, cancellations and chats per end user), graded by a model judge, sent through the API exactly as the customer's integration sends them (no view):

Questions0.10.20.10.3
Recommendations: what the user is likely to want next (12)012
Names of recent events (12)511
Summary of the user's recent activity (12)512
Event linked to a conversation (4)14
Did the user ever ... (absence, 10)710
Next best action (11)1011
Transaction activity (11)1011
Activity in a time window, latest activity, latest chat, latest conversation, recently viewed items (43)4343
Total (115)81114

The best earlier run of the same set (a v0.8.x build, July 2026) was also 114. The recency tail served 83 of the 115 questions (none on 0.10.2). The one miss listed one event name more than the expected answer; its prompt is token-identical to two earlier runs of the same change that scored 115 of 115, so it is answer-model or judge variance.

With the customer's console request shape (view: "holistic", where 0.10.2 already served the tail): 0.10.2 answered 111 of 115. 0.10.3 answered all 77 questions of the eight categories where 0.10.2 had failed any (0.10.2: 73 of the same 77); the other 38, all correct on 0.10.2, were not re-run.

Per answer, no view: about 7,434 input and 291 output tokens (0.10.2: 6,957 and 261); with the answer model used, $0.0445 against $0.0413. Latency with 4 concurrent clients: p50 4.9 s, p90 8.9 s, p99 10.9 s (0.10.2: p50 5.6 s, p90 10.2 s).

Conversation histories (LongMemEval)

The inferred tail must not reach chat histories. On the 38 LongMemEval questions that carry a recency cue ("currently", "recent", "latest", "most recent"), a build that applied the inferred tail everywhere answered 17 of them from the newest 100 turns, and their contexts held 0 of 42 gold evidence turns (0.10.2: 39 of 42). 0.10.3 serves those 17 no tail: 15 of 17 contexts are byte-identical to 0.10.2's and the other 2 hold the same evidence (37 of 42 gold turns in context on both, on a store built for this check). Answers were not re-run on the final build.

On the 30 abstention questions (one store, both binaries, the same answer configuration), no answer changed because of the premise-guard change on the release candidate; the other differences between the two runs had byte-identical contexts (answer-model variance).

Indexed captures under forgets

4 CPUs, 3 GB, real disk syncs, two pairs of runs in reversed order: new-scope wait=indexed p50 77 ms on both; p99 287 ms against 279 ms on 0.10.2 (+7.7 ms, +2.8 %; 95 % bootstrap interval -0.1 to +15 ms). Without forgets p99 was 5.5 ms lower on 0.10.3. No write took over 1 s and every write answered 200.

Other checks

The live Docker suite (six scenarios), the multilingual smoke (42 of 42), the upgrade and rollback probe (72 of 72, "Rollback"), the enrichment-off compaction check (31 LLM calls over 3 passes on 0.10.2, 0 on 0.10.3, 31 again with CORTEX_CONSOLIDATION_ENABLED=1), the rejected-request envelopes and their log fields (8 of 8 failed requests logged with error_code; 0 on 0.10.2), and uptime_seconds.

The final build

The review fixes (the inferred-tail restriction, the tail grounding fallback, the absence-report clause cut and the rejected requests' ids) were checked without answer-model calls (skip_answer_llm) against the candidate on the same stores. On the customer-journey set the answer context of 114 of 115 questions is byte-identical to the candidate's in both request shapes, and the tail serves the same 83 (no view) and 85 (view: "holistic") questions; the 115th context is ranked and also differs between two runs of the candidate itself. That question is the only one whose candidate answer the clause cut shortens; it answered correctly in 2 of 2 runs on the final build. The release commits after the final build change documentation only.

Environment variables

VariableChange
CORTEX_ANSWER_CACHENew. 1 turns the answer cache on (default off).
CORTEX_ANSWER_CACHE_TTL_SECSNew. Default 300, at most 540.
CORTEX_ANSWER_CACHE_MAXNew. Default 1,000 entries, at most 100,000.
CORTEX_ANSWER_KIND_COVERAGENew. Event-kind coverage, default on; 0/off/false turns it off.
CORTEX_UPDATER_URL, CORTEX_UPDATER_TOKENNew. The updater sidecar and its shared token.
CORTEX_CONSOLIDATION_ENABLEDUnset now follows enrichment (was: on). 1 opts in; 0/false/off turns consolidation off.
CORTEX_RECENCY_ORDERUnchanged switch; the tail it controls now serves granular, lineage and raw too.
CORTEX_RECENCY_INFERENCEUnchanged switch; an inferred order outside holistic takes the tail only on typed event streams.

docs/generated/env-vars.md has the full rows.

Error codes

HTTPCodeWhereChangeRetriable
400INVALID_BODYany v1 route that reads a JSON bodyWas a plain-text 400: the body is not JSON, has trailing bytes, could not be read or is not UTF-8.no
400INVALID_QUERYroutes with a query stringNew: a query parameter that does not parse, or a missing required one. A repeated ?scope= on GET /v1/events/{id} is one.no
400INVALID_PATH_PARAMSroutes with a path parameterNew: a path parameter that does not parse.no
400, 422INVALID_REQUESTany v1 routeNew: any other plain-text 400 or 422.no
415UNSUPPORTED_MEDIA_TYPEroutes with a JSON bodyWas plain text: a missing or non-JSON Content-Type.no
422INVALID_BODYroutes with a JSON bodyWas plain text on most routes: an unknown field, a wrong type, a missing field.no
422DOWNGRADE_REFUSEDPOST /internal/ui/updateNew.no
503UPDATER_NOT_CONFIGUREDPOST /internal/ui/updateNew.no

Startup warnings

  • CORTEX_ANSWER_CACHE is on, but this server runs the legacy WAL (CORTEX_UNIFIED_WAL=0), which has no indexing watermark; answers are never stored: the cache is set but cannot store.
  • The CORTEX_VECTOR_RESIDENT_MAX is unset lint now says that a host at or below the RAM model's ~18 GiB baseline gets no estimate, instead of "fits roughly 0M hot vectors".

Rollback

0.10.3 adds no column family, directory or on-disk format. To roll back, stop 0.10.3 with the usual 120 s grace and start 0.10.2 on the same data dir. A rollback brings back 0.10.2's behaviour, including the issues this release fixes. 0.10.2 ignores the stored extract directive, so an event it indexes after the rollback runs every stage again; the answer cache, held in memory, is gone with the process.

What was checked, on one data dir, with a graceful stop between each step (on the release candidate before the review fixes, which change no storage code):

  1. 0.10.2 wrote English, Devanagari and Hinglish captures, a 32 KB turn, a 13 KB event captured on someone else's behalf and a 200-item bulk, and ran a forget-all and a forget by id.
  2. 0.10.3 read all of it, wrote single captures, a 300-item bulk and a multi-chunk Hindi document, and ran a forget-all and a forget by id.
  3. 0.10.2 again opened the store and served everything 0.10.3 had written (the bulk samples, the long document by id); what 0.10.3 had forgotten stayed gone. It wrote, bulk-wrote 100 items and forgot the document by id.
  4. 0.10.3 again read everything, including the writes made during the rollback, and the forgotten items stayed gone.

Every phase stopped cleanly and logged no error. After each phase the column families of every RocksDB store and the data dir's top-level entries were listed: 0.10.3 added none, before or after the rollback (72 of 72 checks). A rollback from 0.10.3 to 0.10.1 or v0.9.13 was not re-run; 0.10.3 stores nothing 0.10.2 does not, so RELEASE_NOTES_0.10.2.md, "Rollback", describes it.

Known issues

New or changed in 0.10.3:

  • An inferred timeline tail on a typed event stream still replaces ranked retrieval. A recency-shaped question there gets the newest events (plus the kind exemplars) even when it is about an older one. Send order: "relevance" for such questions.
  • The typed-event-stream test is a heuristic: a conversation integration that sets context.intent on most messages gets the inferred tail too. Send order: "relevance", or set CORTEX_RECENCY_INFERENCE=0.
  • budgets.max_tokens does not cap a /v1/recall tail. An explicit order: "recency" in granular, raw or lineage serves the tail as holistic always has, up to CORTEX_SMALL_SCOPE_EXHAUSTIVE_MAX_TOKENS or the tail size, whichever is smaller. /v1/answer still cuts its context to budgets.max_tokens.
  • Tail and kind-coverage entries carry no timestamp prefix, so a "when" question over a conversation tail sees no dates unless the text has them.
  • A lineage tail lists the request scope's own events (plus recent writes, through the recent-write supplement): the ancestors' older events are not in it. Send order: "relevance" for a lineage read that needs them.
  • prefer_exhaustive is ignored on leaf scopes: the whole-corpus probe runs only in holistic, and an exact-scope answer recalls granular.
  • CORS does not expose x-cortex-answer-cache or allow the Cache-Control request header, so a browser client cannot read or set them across origins.
  • The startup lint sizes RAM from the host's MemTotal, not a container memory limit.
  • Consolidation's entity-extractor calls are not booked in GET /v1/admin/usage: with CORTEX_CONSOLIDATION_ENABLED on, only the consolidation calls themselves appear.

Carried over from 0.10.2 (still present in 0.10.3):

  • /v1/answer accepts unknown body fields (200, the field ignored); /v1/recall answers malformed JSON with 422 INVALID_BODY, not 400, and accepts a body with no Content-Type.
  • A 413 for an oversized body has no CORS headers.
  • wait=indexed waits for the whole log, not for the capture's own event; a storm of new scopes inside a forgotten subtree still blocks the forget; forgets contend with long-document fragment locks. All three are known and deferred.
  • Everything else under "Known issues" in RELEASE_NOTES_0.10.2.md, except the plain-text 422, which 0.10.3 fixes.

Clients

  • TypeScript SDK cortexdbai 0.10.1 (npm, from 0.10.0): types only. ExtractStage is exported, RecallEventFields.extract (ExtractStage[]) is on events and recall items, and AnswerResponse.diagnostics.answer_cache (hit, age_ms) on a cached answer. No runtime change.
  • Python SDK cortexdbai 0.12.0, cortexdb-mcp 0.8.0, cortexdb-cli 0.6.0 and cortexdb-connectors 0.2.22 are unchanged and work against 0.10.3. What they will see:
    • a rejected request (400, 415, 422 before the route runs) now raises with the server's error_code (INVALID_BODY, INVALID_QUERY, ...) instead of UNKNOWN, still as the base V1APIError, so existing handlers keep working;
    • the SDKs and the CLI send view: "holistic" on answer by default, so their inferred recency behaves as in 0.10.2; only a recency tail that names none of the question's capitalised names now falls back to ranked retrieval. None of them exposes order yet;
    • code that compares an answer with "The information provided is not enough." must match the prefix (see "Before you upgrade");
    • the connectors classify errors by HTTP status, so the envelope change is transparent to them.

Notes

Single-node remains the only supported topology.

v0.10.2

indexed captures that do not queue behind disk syncs, one wait deadline, forgets that survive scope registrations, and the captured speaker on every recalled event

0.10.2 is a fix release on top of 0.10.1, the last published release. Everything in RELEASE_NOTES_0.10.1.md still applies; this page lists only what changed since. It adds no column family, directory or on-disk format, and nothing is migrated: a 0.10.1 server opens and serves a data dir 0.10.2 has used (see "Rollback").

Full notesHide notes98 notes

Summary

  • wait=indexed captures no longer queue behind other writes' disk syncs. The indexer and the bulk capture path write each batch to the log with one durable write instead of one disk sync per event. On a disk with ~3 ms syncs, a capture into a new scope during a 400-memory seed went from p99 1.1 s (max 1.4 s) to p99 0.28 s (max 0.46 s), and a 500-item /v1/experience/bulk from about 3 s to 0.25-0.5 s to acknowledge.
  • One wait deadline. A ?wait= that runs out answers 408 WAIT_TIMEOUT after about 30 s, not 60 s, says how long it waited and on what, and a bulk's budget now covers its inline indexing (the 408 then lists the batch's event ids).
  • Forgets and erasures are no longer refused by an unrelated scope registration. A capture that registered a brand-new scope anywhere while a forget was being authorized made the forget answer 403 POLICY_DENIED with nothing done. It now proceeds; a change inside its own footprint re-plans, and at worst answers a retriable 409 AUTHORIZATION_STATE_CHANGED.
  • Recall reports who said each event. Several recall paths (long events found through a later chunk, the holistic view's own-scope events, prefer_exhaustive, order=recency) reported the capturing caller as observed_actor and an unknown subject. Every recalled event now carries the identity it was captured with, the same as GET /v1/events/{id}, and a cancelled capture leaves every recall view.
  • GET /v1/events/{id}?scope= confines a by-id read to one exact scope (new capability event_scope_filter_v1).
  • budget_excerpt_v1 is withheld while CORTEX_RECALL_BUDGET_EXCERPTS is off, so a client that gates on it knows when excerpts are off.
  • A log line per request (CORTEX_REQUEST_LOG, default slow), with the time spent in each stage, and the same request id on the audit rows a forget writes.
  • Fixes found after 0.10.1 shipped: embedding calls that asked for more than the client accepts in one response (a long document could stall indexing), empty descendant recall after a store grew past a size threshold while serving, the admin console's "degraded" label, and log noise from recall candidates a concurrent forget had just removed.
  • No client changes. Every client released with 0.10.1 works against 0.10.2 unchanged (see "Clients" for what they will see differently).

Upgrade from 0.10.1

Before you upgrade: what clients and operators will notice

  • 408 WAIT_TIMEOUT comes at about 30 s, not about 60 s. A single ?wait=indexed or consolidated capture has one budget for both the indexer wait and the stage wait. details gain waited_ms and waiting_on (indexer or stage). A client whose own timeout is a little over 30 s now gets the 408 instead of timing out itself. wait=consolidated now has 30 s in total for indexing and consolidation, so a deployment with an enrichment delay or scope-quiet dispatch sees more 408s there. The budget starts after admission: a capture into a scope a running forget covers can first wait up to CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS (default 120 s) to be admitted, as in 0.10.1.
  • A bulk ?wait=indexed|consolidated can now answer 408 while it is still indexing. Its one budget, min(30 s + 1 s per item, 600 s), now covers the inline indexing, which used to have no limit (such a batch waited and then answered 200 with its ids). The 408's details.event_ids lists the batch's events once they are written; poll them with GET /v1/experience/status?event_id=.
  • Resending after a 408 replays only what carried an idempotency_key. The 408 messages now say so: an omitted key is minted by the server and never returned, so a keyless resend writes again. Send an idempotency_key on every capture that waits, bulk items included.
  • The deprecated legacy dual-WAL backend (CORTEX_UNIFIED_WAL=0) answers its own wait timeout with 408 WAIT_TIMEOUT instead of 500 INTERNAL.
  • 409 AUTHORIZATION_STATE_CHANGED where a 403 or 404 used to come back. On POST /v1/forget, /v1/forget/preview, /v1/erasures, /v1/erasures/preview, GET /v1/erasures/{id} and its cancel, when scopes were registered or removed inside the request's footprint on each of 5 attempts. It is retriable: Retry-After: 1 and details.retry_after_seconds. Nothing was changed; re-send the identical request. A client that treated the old spurious 403 as final now gets a retriable answer instead.
  • Recalled events carry their captured identity. On the paths listed above, layers.events[].observed_actor, subject, scope_key and wal_offset change from the capturing caller, {id: "unknown"}, the scope string and a per-partition sequence to the captured values, equal to GET /v1/events/{id}. So a filters.metadata.observed_actor recall now keeps events captured on someone's behalf that it used to drop, and a client that compared scope_key with scope on those items sees scope_key change to the billing partition the event was captured under (for example goldenco for an event in org:goldenco/user:alice). context_block bytes do not change.
  • A cancelled capture leaves every recall view. POST /v1/lifecycle/memory-event/{id}/cancel already hid the event from granular recall and GET /v1/events; it now also leaves the default holistic pack, prefer_exhaustive and order=recency.
  • The Docker image sets CORTEX_EMBEDDING_MAX_BATCH_ITEMS=256 (it was 2048), and the OpenAI-compatible client clamps any value to what fits its 16 MiB response cap (340 texts at 1536 dimensions, 170 at 3072). A long document therefore makes more, smaller embedding calls.
  • The default request log writes a line for every response outside 2xx, unauthenticated 401 probes included, plus every request slower than 1 s. A proxy or health checker that calls an authenticated route without a key writes one line per call. Set CORTEX_REQUEST_LOG=off to turn it off, or raise CORTEX_REQUEST_LOG_SLOW_MS.
  • The image's RUST_LOG adds cortex_embedding=warn, so the embedding clamp and retry warnings reach the container log. A deployment that sets its own RUST_LOG keeps it.
  • Forget-all latency under concurrent writes on a slow disk is about twice 0.10.1's (see "Performance"). With no concurrent writers, or on NVMe, it is unchanged.
  • Holistic and order=recency recall over large subtrees cost a little more: each event those paths add is now one point read of the captured event (measured +11% p50 for holistic recall at a parent of 3,000 events, +23% for order=recency over 2,000 events). Ranked granular recall is unchanged.
  • capabilities[] lists 11 names: event_scope_filter_v1 is new, and budget_excerpt_v1 is withheld while CORTEX_RECALL_BUDGET_EXCERPTS is 0, off or false.

Upgrade steps

Nothing is migrated and there is nothing to run afterwards. Replace the binary or image and restart as for any release, giving the server 120 s to stop (as in 0.10.1: it waits for running forgets and erasures and persists its snapshot). Then check:

  1. GET /v1/admin/version: crate_version 0.10.2, the git_sha you deployed, and event_scope_filter_v1 in capabilities[].
  2. GET /v1/admin/ready: ready: true.
  3. The log: if you set CORTEX_EMBEDDING_MAX_BATCH_ITEMS yourself, a CORTEX_EMBEDDING_MAX_BATCH_ITEMS=<n> clamped to <m> warning tells you the value in effect. At 3072 dimensions the image's own 256 is above the bound, so the warning appears with no setting of yours; it is harmless.
  4. If the request log is too verbose for your log pipeline, set CORTEX_REQUEST_LOG=off.

Upgrading from v0.9.13

0.10.2 contains everything in 0.10.1. Read RELEASE_NOTES_0.10.1.md first ("Upgrade from v0.9.13", "Plan the first start", "Upgrade steps"): its first start, background work and upgrade steps are the same for 0.10.2, and this page's "Before you upgrade" applies on top.

Performance

How it was measured: one host, one server container at a time (4 CPUs, 3 GB of memory, mock embeddings, a fresh data dir), 0.10.1 and 0.10.2 alternated with the same client script. The disk matters: the gains come from fewer disk syncs, so they show on a disk whose sync takes milliseconds (here EBS gp3, 2.9 ms per 4 KiB synced write) and vanish on a fast one (NVMe, 0.13 ms). Your absolute numbers depend on your disk; the shape (latency that no longer grows with the size of the burst ahead of you) is what carries over.

Indexed captures into new scopes

Each round, four clients seed 400 memories into one scope while six clients capture with wait=indexed into brand-new scopes, pausing 0.25 s between captures (the "no pause" row drops the pause). In the rows "with forgets", a forget-all of the seeded scope runs during the seed and another after it:

wait=indexed capture, EBS0.10.10.10.2
p50 / p90 / p99 / max (ms), with forgets (10 min)66 / 546 / 1120 / 140372 / 156 / 276 / 460
rounds whose p99 passed 1 s47 of 1220 of 105
captures over 500 ms / over 1 s, with forgets813 / 1630 / 0
p50 / p90 / p99 / max (ms), no forgets (6 min)94 / 1119 / 1569 / 184152 / 119 / 178 / 278
captures over 1 s, no forgets3940
p50 / p90 / p99 / max (ms), with forgets, no pause (5 min)161 / 282 / 794 / 1372159 / 276 / 371 / 992
indexer cost per event in a page5.2 ms0.47 ms

With real disk syncs, 0.10.2's tail (p99 178 ms, no forgets) is below what either build reaches with disk syncs turned off (CORTEX_WAL_SYNC=0, a diagnostic setting that gives up durability: p99 264 ms on 0.10.1, 294 ms on 0.10.2). On NVMe the two builds measure the same (p50 15 ms, p99 95 vs 93 ms).

Bulk captures

500-item /v1/experience/bulk, idle server, EBS, 6 requests0.10.10.10.2
no wait (ms)2953-3158240-412
wait=indexed (ms)3157-3222425-509

A bulk's log write is made while the indexer holds back every newer write, so the other clients' wait=indexed captures used to wait out one disk sync per bulk item. Six new-scope wait=indexed writers beside two clients sending bulks back to back (90 s per bulk size; the first column is a 0.10.2 candidate that already had the indexer change but not yet the bulk one):

Bulk sizeSingles p50 / p99 (ms), indexer change onlySingles p50 / p99 (ms), 0.10.2Bulk log write while the indexer waits, p50 / max (ms)
none24 / 3223 / 31-
50200 / 34468 / 181284 / 327 → 5 / 15
200794 / 1534284 / 439719 / 1149 → 7 / 13
50048 / 371554 / 9942520 / 2795 → 12 / 15

What is left of the singles' tail is the bulk's own indexing (its embedding call and index writes: about 1 s for 500 items here), because wait=indexed waits for the whole log (see "Known issues").

Wait deadline

With an embedding provider that answers 503 to every call, a wait=indexed capture:

0.10.10.10.2
answer408 at 60.0 s, message "timed out after 30s"408 at 30.0 s, waited_ms: 30003, waiting_on: "indexer"

A bulk wait=indexed fails fast with 502 INDEXING_FAILED on both.

Forget and erasure

Forgets and erasures sent while other clients registered new scopes:

0.10.10.10.2
forget-alls beside 12 clients registering new scopes in the same organisation39 of 52 refused 40341 of 41 done at the first attempt (p50 374 ms)
forget-alls beside writers with no pause (arm above)26 403s; 10 of 70 never succeeded in 2 attempts50 of 50 done at the first attempt
forgets beside about 6.5 new child scopes per second inside the forgotten subtree283 of 283287 of 287 at the first attempt
POST /v1/erasures beside the registrations20 refused 40367 accepted, 2 409 (retriable)
GET /v1/erasures/{id} polls11 spurious 404s in 5400 in 670

Forget-all latency (ms, answered 200) has a trade-off on a slow disk: the indexer no longer stalls, so a forget's search-index commit now competes with it for the index writer.

Forget-all p50 / p90 / p99 / max (ms)0.10.10.10.2
EBS, beside the seeding writers108 / 209 / 301 / 341221 / 310 / 372 / 408
EBS, beside writers with no pausep50 115p50 299
EBS, no concurrent writers151 / 210 / 224 / 382155 / 227 / 258 / 383
NVMe, beside the seeding writers50 / 69 / 86 / 10251 / 79 / 96 / 108

Long documents and embedding calls

One 1,000,000-byte event (2,061 chunks) at 1536 dimensions, against an OpenAI-compatible endpoint:

0.10.1 (batch 2048)0.10.2 (image default 256)0.10.2 with 2048 set (clamped to 340)
provider calls110, each response 64.6 MB and rejected9, largest response 8.1 MB7, largest response 10.7 MB
capture408 at 60 s; the indexer kept retrying200 in 1.6 s200 in 1.6 s

What changed

Capture and indexing: one durable write per batch

A wait=indexed capture waits until the indexer has applied every write logged before it, in every scope, not only its own event. The indexer applies the log in pages, and each page wrote its derived rows with one disk sync per event: a 256-event page cost 256 syncs. On a disk with millisecond syncs, a capture that landed behind a burst of writes (a seed, a connector backfill) waited seconds for them. /v1/experience/bulk did the same with its captured events, while holding back the indexer from every newer write.

Both now write a batch with one durable write: one write of the whole batch, then one sync, before anything is acknowledged. A crash keeps either the whole batch or none of it, and offsets are never reused or skipped. The stored rows are the same as before.

wait=indexed is still a wait for the whole log: a capture that lands behind a bulk waits for that bulk's indexing, its embedding call included. Use wait=accepted and poll GET /v1/experience/status when you do not need read-your-writes.

One wait deadline

  • Single writes. The indexer wait and the stage wait after it share one budget of about 30 s. A capture the indexer has not reached when it runs out answers at once; it used to wait a second 30 s for a stage it could no longer reach, and its message said 30 s. details: event_id, target_stage, status_url, waited_ms, waiting_on (indexer or stage).
  • Bulk. wait=indexed|consolidated has one budget of min(30 s + 1 s per item, 600 s) for the inline indexing and the stage barrier together. When it runs out during the indexing, the 408 (waiting_on: indexing, items) lists the batch's captured events in details.event_ids (distinct, in item order, the list a 200 returns) as soon as the batch has been written; the indexing finishes in the background. During the stage barrier it is waiting_on: stage with accepted and the same event_ids. wait=settled keeps its settlement deadline; a bulk with no wait has no deadline, as before.
  • Legacy dual WAL. The mirror's own wait timeout is a 408 with {target_stage, waited_ms, waiting_on: "indexer"}, not a 500.
  • What to do after a 408. The write is captured and durable. Poll status_url (single) or each id in event_ids (bulk). A resend replays, never duplicates, only when it carries the same idempotency_key; a capture sent without one gets a server-minted key the client never sees, so resending it writes it again.

Forget and erasure: a scope registration no longer refuses a forget

A forget, an erasure and their previews check the caller's authorization once, then take a lease on their footprint. The lease used to be refused whenever anything in the scope registry had changed in between, and every capture that registers a brand-new scope, from any tenant, makes such a change. The request then answered 403 POLICY_DENIED, with nothing done, and the same race made GET /v1/erasures/{id} and its cancel answer a spurious 404.

The lease now compares the plan's own authorization boundaries, so a scope registered outside the footprint does not matter. When a boundary appears or vanishes inside it, the request re-plans on the current state (a caller the new boundary denies still gets 403, a job it cannot see still 404) and tries again, up to 5 attempts, then answers 409 AUTHORIZATION_STATE_CHANGED (retriable, Retry-After: 1). Registry errors that are not a denial keep their own status instead of a 403. Each refusal is logged at warn with its reason. An erasure refused this way marks its job failed with nothing erased; a retry with the same idempotency_key restarts that job and runs it, while a retry without one leaves a failed job record per refused attempt.

Diagnostics:

  • The forget's info lines carry elapsed_ms (monotonic, since the erasure began) and step_ms; the "fragments erased" line adds bm25_publish_ms and bm25_pending_adds_published (the pending search-index additions the forget's commit published).
  • The "erasure residual: pass complete" line adds per-step durations (content_flush_ms, content_compact_ms, wal_ms, fragments_ms, fulltext_ms, fulltext_gc_ms, derived_flush_ms), fulltext_merge_error (with the segment) and merge_vanished. A segment that a running search-index merge took, or that a delete emptied, is counted as vanished and carried to the next pass, not reported as a merge error.
  • The request log line of /v1/forget, /v1/erasures and their previews carries lease_wait_ms, blocking_queue_ms and engine_ms.

Recall: the captured identity for every event

Some recall paths built a layers.events item from an older internal record that holds only the capturing caller, while GET /v1/events/{id} read the captured event:

  • an event longer than CORTEX_PARENT_HYDRATE_MAX_BYTES (6,144 bytes) found through a later chunk;
  • the default holistic view's events of the requested scope itself;
  • prefer_exhaustive and order=recency;
  • ancestor events, lineage, the evidence boost and artifact sources.

Those items reported the caller as observed_actor, {id: "unknown"} as subject, the scope as scope_key and a per-partition sequence as wal_offset, and a filters.metadata.observed_actor recall dropped them. Every item now carries the captured event's identity. A capture whose stored copy is redacted is never served from the older record instead, and a cancelled capture now leaves the holistic, exhaustive and recency packs too. context_block bytes are unchanged. This had been wrong since v0.7.0.

Checked on a 13,080-byte event captured on behalf of another person and hit at deep chunks, under granular and holistic recall, with prefer_exhaustive, order=recency, a 90-event holistic scope and an observed_actor filter: every item's caller, observed_actor, subject, scope_key, scope and wal_offset equal GET /v1/events/{id}. On 0.10.1 all eight checks fail. The correct identity is also served for long events a 0.10.1 server wrote.

Recall: candidates dropped for want of content

A search-index hit carries no text; its text comes from the content store. A forget deletes the content first and publishes the search-index delete at the end, so a recall running meanwhile can find a hit with no content. It drops it (nothing is ever served) and used to warn "dropped candidates with missing or tenant-mismatched content". Now:

  • a hit whose content an erasure removed during the recall, or shortly before, logs at debug;
  • a hit with no content and no erasure behind it (an orphan index document) warns;
  • a hit whose content belongs to another tenant warns.

GET /v1/admin/metrics counts them since process start under recall_empty_content_drops (absent_during_erasure, absent_orphan, foreign_tenant).

Recall: after a store grows past the scope-map threshold while serving

A store whose log crossed 250,000 entries while serving, with no reader having built the scope-partition map while it was smaller, never got the map until the next restart, so the holistic view's descendant events came back empty (8 of 8 recalls over 35 s, found on a bulk re-ingest into a fresh data dir). The first check past the threshold now builds the map in the background (about 1 s for 266,000 entries); only the recalls in that window are degraded, and every later one serves the descendant events.

GET /v1/events/{id}?scope= (event_scope_filter_v1)

?scope=<scope> returns the event only when its own scope is exactly that scope. An event in a sibling, ancestor or descendant scope is the same 404 NOT_FOUND as a missing id, byte for byte. An empty, blank or malformed value is 422 INVALID_SCOPE_GRAMMAR, and a repeated scope parameter is refused with a plain-text 400: never a read without the filter. Without the parameter nothing changes; note that the static operator key and an auth-disabled server read every scope of every tenant by id. A 0.10.1 server ignores the parameter, so send it only when capabilities[] lists event_scope_filter_v1, or compare the returned scope yourself. It does not apply to GET /v1/facts/{id}.

Capabilities

GET /v1/admin/version lists 11 names: 0.10.1's ten plus event_scope_filter_v1. budget_excerpt_v1 is the only name an operator switch withdraws: it is not listed while CORTEX_RECALL_BUDGET_EXCERPTS is 0, off or false, the setting that restores the 0.10.0 render, and the startup config lint logs RECALL_BUDGET_EXCERPTS_DISABLED. In 0.10.1 the name stayed listed with the switch off, so a client that trusted it expected contexts inside their budget and did not get them. Every other name stays listed whatever the switches say.

NameNew inWhat it tells a client
event_scope_filter_v10.10.2GET /v1/events/{id} honours ?scope= (exact match)
budget_excerpt_v10.10.1No context entry passes budgets.max_tokens; now absent while the switch is off
the other nine0.10.1Unchanged (RELEASE_NOTES_0.10.1.md, "Capabilities")

Request log and request ids

CORTEX_REQUEST_LOG (off, slow or all; default slow) writes one info line per request under the target cortex_api::request, which needs cortex_api=info in RUST_LOG (the image's default has it):

request request_id=req_0197… method=POST route=/v1/experience status=408 total_ms=30009
  admission_ms=4 lease_wait_ms=0 wal_append_ms=2 indexer_wait_ms=30000 queue_depth=412
  outcome="timeout"
  • request_id is the id the response echoes in X-Request-Id (your own, when you send one); client_request_id appears when an error response minted another. route is the route template, never the path; status is 0 when the client went away first.
  • Stage timings appear when the stage ran: admission_ms (the scope check and any auto-registration), lease_wait_ms (for a write, its admission time when a running forget covered its scope: an upper bound of the wait), wal_append_ms, indexer_wait_ms, queue_depth (the unindexed writes ahead of and including this one), stage_wait_ms, and for forgets and erasures lease_wait_ms, blocking_queue_ms and engine_ms.
  • outcome is ok, timeout (a wait ran out) or client_gone.
  • slow logs requests of at least CORTEX_REQUEST_LOG_SLOW_MS (default 1000), every status outside 2xx and every outcome other than ok; all logs every request; off does no work.
  • No body, query string, path value or header other than X-Request-Id is read.

The same id is now on the audit rows the request writes: the authentication layer's row and the gdpr: true row of POST /v1/forget. A forget's audit trail, its log line and your own report can be joined on it. (Before, the gdpr row always, and the authentication layer's row on a successful request, carried a fresh id that appeared nowhere else.)

On the measured runs the line accounted for where a slow capture spent its time: in the seeding test above, 0.10.2's slowest 1% of captures spent a median 245 ms in indexer_wait_ms and 43 ms in admission_ms, and client and server times agreed within 2 ms at p50.

Embedding batches within the response cap

With CORTEX_EMBEDDING_MAX_BATCH_ITEMS=2048 (the 0.10.1 image default), one call for a long document asked for more than the embedding client accepts in one response (a 630-chunk event at 1536 dimensions is about 21 MB of formatted numbers against a 16 MiB cap). The call failed and was retried unchanged, and indexing stalled on that event. Now:

  • the OpenAI-compatible client reports the most texts per call whose response fits the cap, floor(12 MiB / (24 × dimensions + 128)): 340 at 1536 dimensions, 170 at 3072, 2006 at 256;
  • the configured value is clamped to it on every flush, and a value set above it logs one warning at startup (CORTEX_EMBEDDING_MAX_BATCH_ITEMS=<n> clamped to <m> ..., target cortex_embedding);
  • an over-cap response reads as "too large", so the batch is split in half instead of re-sent;
  • the Docker image sets 256.

Admin console

The readiness pill said "degraded (mock embeddings)" for every degraded state, including a failing embedding provider (for example an expired key answering 401). It now names the reason (mock embeddings, or embedding calls failing) and shows the server's detail on hover.

API contract text

/v1/openapi.json descriptions are rewritten for API users (no internal project codes, ticket ids or storage internals). POST /v1/forget now references ForgetRequest like its preview does (only scope is required; reason is an alias of audit_note; from_preview_id is documented; unknown fields are refused, as the server always did), which tightens clients generated from the spec. POST /v1/answer marks question_type deprecated and ignored and documents skip_answer_llm, conversation_id, view, order, budgets and include. The new 408s and 409s are declared, with Retry-After on the 409s, and GET /v1/events/{id} documents ?scope=.

Environment variables

VariableChange
CORTEX_REQUEST_LOGNew. off, slow (default) or all; see "Request log and request ids".
CORTEX_REQUEST_LOG_SLOW_MSNew. The slow threshold, default 1000 ms.
CORTEX_EMBEDDING_MAX_BATCH_ITEMSThe image default is 256 (was 2048; the binary's own default stays 2048); the OpenAI-compatible client clamps it to its response bound.
CORTEX_RECALL_BUDGET_EXCERPTS0/off/false now also withholds budget_excerpt_v1 and logs a startup lint.
RUST_LOGThe image default adds cortex_embedding=warn (so does the binary's built-in default).

docs/generated/env-vars.md has the full rows.

Error codes

HTTPCodeWhereChangeRetriable
408WAIT_TIMEOUTPOST /v1/experience, /v1/experience/bulk with ?wait=One budget per request (about 30 s single; min(30 s + 1 s per item, 600 s) bulk, inline indexing included). details.waited_ms, details.waiting_on; bulk adds event_ids. The legacy dual-WAL mirror's wait timeout is a 408, not a 500.yes: poll, or resend with the same keys
409AUTHORIZATION_STATE_CHANGED/v1/forget, /v1/forget/preview, /v1/erasures, /v1/erasures/preview, GET /v1/erasures/{id} and its cancelNew on these routes. Replaces a spurious 403 POLICY_DENIED or 404. Retry-After: 1, details.retry_after_seconds. (On /v1/recall and /v1/answer the same code remains a 503.)yes
422INVALID_SCOPE_GRAMMARGET /v1/events/{id}New there, for an empty or malformed ?scope=.no

Startup warnings

  • RECALL_BUDGET_EXCERPTS_DISABLED: CORTEX_RECALL_BUDGET_EXCERPTS is off, so recall renders as 0.10.0 did and budget_excerpt_v1 is not listed.
  • CORTEX_EMBEDDING_MAX_BATCH_ITEMS=<n> clamped to <m> (target cortex_embedding): the value in effect is below the one configured.

Rollback

0.10.2 adds no column family, directory or on-disk format; its batched log writes store the same rows a one-by-one write stores. To roll back, stop 0.10.2 with the usual 120 s grace and start the 0.10.1 build on the same data dir. A rollback brings back 0.10.1's behaviour, including the issues this release fixes (and, on the 0.10.1 image, its embedding batch default of 2048). A client that gates on capabilities[] stops sending ?scope= by itself; one that does not keeps sending it, and 0.10.1 ignores it.

What was checked, on one data dir, with a graceful stop between each step:

  1. 0.10.1 wrote English, Devanagari and Hinglish captures, a 32 KB turn, a 13 KB event captured on someone else's behalf and a 200-item bulk, and ran a forget-all and a forget by id.
  2. 0.10.2 read all of it (a budget excerpt, ?scope= giving 200 for the event's own scope and 404 for an ancestor, and the correct identity for the long event 0.10.1 wrote, which 0.10.1 itself had served with the caller's identity), wrote single captures, a 300-item bulk and an 11-chunk Hindi document, and ran a forget-all and a forget by id.
  3. 0.10.1 again opened the store and served everything 0.10.2 had written (the bulk samples, the long document by id); what 0.10.2 had forgotten stayed gone. It wrote, bulk-wrote 100 items and forgot the document by id.
  4. 0.10.2 again read everything, including the writes made during the rollback, and the forgotten items answered 404.

Every phase stopped cleanly (exit 0) and logged no error. After each phase the column families of every RocksDB store (from its latest options file) and the data dir's top-level entries were listed: 0.10.2 added none, before or after the rollback. The final 0.10.2 build passed all 72 checks.

A rollback from 0.10.2 straight to v0.9.13 was not re-run. 0.10.2 stores nothing 0.10.1 does not, so RELEASE_NOTES_0.10.1.md, "Rollback", describes it, with its limits.

Known issues

New or changed in 0.10.2:

  • A storm of new scopes inside the forgotten subtree still blocks a forget. Under about 28 new child scopes per second inside the footprint, every re-plan found the footprint moved again: 6 of 6 forgets exhausted 10 client attempts with 409 AUTHORIZATION_STATE_CHANGED for as long as the storm lasted. 0.10.1 answered 403 there, so neither makes progress; about 6.5 per second converged at the first attempt (287 of 287). Pause the captures into the subtree, or retry after them.
  • wait=indexed still waits for the whole log, not for the capture's own event: a capture that lands behind a large bulk waits for that bulk's indexing (about 1 s for a 500-item bulk in the measurement above), so its tail still grows with the size of concurrent bulks.
  • A bulk is slow beside a stream of new-scope captures. A 500-item bulk into one existing scope took 7-9 s beside six clients registering new scopes (0.25-0.41 s alone), on 0.10.2 before and after its bulk change. Its admission, log write and indexing account for about 1 s of that; the likely cause, not yet confirmed, is the bulk's per-item scope check after admission, which queues behind the registry rewrite each new scope makes (next item).
  • New-scope capture admission grows with the scope registry: each new scope rewrites the registry snapshot. In a 10-minute test admission p50 grew from 13 to 45 ms (from 31 to 114 ms with writers that never paused), the largest growing term once the disk-sync stall is gone. 0.10.1 has the same cost, hidden behind the stall.
  • Other scope-checked routes can still be refused by a concurrent registration. POST /v1/lifecycle/memory-event/{id}/cancel (a spurious 404), the /v1/conflicts routes (403 or 404), GET /v1/usage and POST /v1/understanding/synthesize still check against the registry state they planned on, so a new scope registered anywhere at that moment refuses them with nothing done. Retry.
  • On /v1/scopes mutations the 409 AUTHORIZATION_STATE_CHANGED and 409 SCOPE_STATE_CHANGED bodies say retriable: false. Nothing was applied; re-sending the mutation is safe.
  • The request log times the response headers. total_ms ends when the headers are sent, so a streamed body (the lifecycle stream, a streamed answer) is logged when it starts, and a client that leaves during the body is still logged ok.
  • More search-index merge warnings. The faster indexer commits more often, so the harmless Tantivy warning "couldn't find segment in SegmentManager" appears more often (87 against 11 in one 10-minute run). No data or latency effect.
  • The deprecated legacy dual-WAL backend (CORTEX_UNIFIED_WAL=0) restarts slowly after repeated hard kills with large events in flight (boot times of about 1 s, 15 s, 40 s, then over 150 s in one test), and an acknowledged capture was not yet listed 120 s after one of them. 0.10.1 behaves the same. Use the default unified WAL.

Carried over from 0.10.1 (still present in 0.10.2):

  • POST /v1/experience?wait=settled cannot succeed: the capture is written, then the request answers 503 SETTLEMENT_UNAVAILABLE. A retry without the same idempotency_key writes it again. Use wait=indexed.
  • The lifecycle_stream link a bulk capture (?batch_id=) or an erasure (?erasure_id=) returns is not filtered by that id: the stream carries every lifecycle event the caller may see. Poll GET /v1/experience/status or GET /v1/erasures/{id} instead.
  • GET /v1/facts?subject= compares the stored subject, which for extracted facts is the extracted name, not the ent_ id the response shows, so filtering by a returned id finds nothing; object is not implemented.
  • A per-request answer_model on /v1/answer is echoed in the diagnostics but not applied.
  • CORTEX_VERIFIER_URL= (empty) does not turn the verifier off: it falls back to the OpenAI URL. To keep the verifier off, leave CORTEX_VERIFIER_API_KEY and OPENAI_API_KEY unset.
  • GET /v1/conflicts/{id} and its resolve answer an unknown id with 404 NOT_FOUND, not the documented CONFLICT_NOT_FOUND.
  • A request body the server cannot parse (for example an unknown field on /v1/forget) is refused with a plain-text 422, not the JSON error envelope.
  • Everything under "Known issues" in RELEASE_NOTES_0.10.1.md.

Clients

No client changes: no client version is bumped, and cortexdbai 0.12.0 (PyPI) and 0.10.0 (npm), cortexdb-mcp 0.8.0, cortexdb-cli 0.6.0 and cortexdb-connectors 0.2.22 work against 0.10.2 unchanged. Every change is additive or on an error path. What clients will see:

  • Python and TypeScript SDKs (default timeout 60 s): a wait that runs out now arrives as a V1APIError with status 408 and error_code WAIT_TIMEOUT at about 30 s, with details.status_url or details.event_ids, instead of a client-side timeout at 60 s. The new 409 AUTHORIZATION_STATE_CHANGED on forget and erasure arrives as a plain V1APIError with retriable true, not as the V1PolicyDeniedError the old spurious 403 raised: retry it after Retry-After.
  • Neither SDK has a method for GET /v1/events/{id}, so ?scope= has no client method yet; call the route directly.
  • cortexdb-cli: its HTTP timeout is 30 s, the same as the server's wait budget, so --wait indexed can still end in the CLI's own timeout rather than the 408. The capture is durable either way; check it with GET /v1/experience/status.
  • cortexdb-mcp: its get-by-id tool reads GET /v1/events/{id} without ?scope=.
  • cortexdb-connectors: a forget that meets the registration race now gets a retriable 409 instead of a 403; the connector's batch stays durable for its next retry, as before.
  • Framework adapters (cortexdbai integrations): their forget tools run a scope-wide forget (cascade: redact_events, confirm_all: true) and ignore the query they take. This is not new in 0.10.2; do not expose those tools to an agent unless a scope-wide forget is what you want.

Notes

Single-node remains the only supported topology.

The single-capture, forget and registration figures above were measured on a 0.10.2 candidate before its last change, which touched only the bulk capture write and the request ids; the bulk figures, the upgrade and rollback check and the smoke tests were run on the final build. The recall-cost figures (+11% and +23%) come from the change's own measurement and were not re-run on the final build.

v0.10.1Breaking

multilingual memory, erasure that reaches every copy, derived-data repair, and recall contexts that stay inside their budget

0.10.1 is the first published 0.10 release. 0.10.0 was prepared but never tagged or published, so this page covers everything that changed since v0.9.13, the last published release. RELEASE_NOTES_0.10.0.md is kept for reference only; everything on it is repeated here, with the fixes made after it.

Full notesHide notes142 notes

Summary

  • Multilingual capture and recall. Memories are captured and recalled in the language they were written in, never translated. Captures carry a language tag, a script list, a time zone and the times they refer to; recall takes language and script hints, up to four query variants (for example an English phrasing of a Hinglish question), lenient time phrases and a referenced-time boost. The measured launch set is English plus Latin-script Hinglish; every other script is captured, tagged and recalled, but cross-script recall is not yet at the quality bar.
  • Capabilities. GET /v1/admin/version now lists capabilities[]. Every new request field belongs to one capability; gate on the list, never on the version string.
  • Erasure that reaches every copy. /v1/forget and /v1/erasures now delete the uploaded blob an event came from, compact the erased bytes out of the storage files, keep an erased fact erased across a crash, report only what they actually removed, select exactly what existed before the request, and cost what they erase rather than what the store holds (a forget by id on a 5M-event store: about 65 s without the new erasure index, under 0.3 s with it).
  • Facts and beliefs (enrichment on). Negation and cessation are kept, different people are no longer merged, non-Latin names get their own entity ids, facts keep their original wording with a byte-exact quote, and beliefs have stable keys and deterministic ids.
  • Repair of data written by earlier builds. New operator routes re-derive facts and beliefs that v0.9.13 and earlier wrote wrongly, from the event log, with no wipe and no re-ingestion.
  • Answer and recall context. /v1/answer contexts built from whole events are packed correctly again; since 0.10.1 no context entry takes context_block past its token budget (an oversized event is served as an excerpt of its best-matching chunks), and every event the block renders is named in the response.
  • Documents. Recall of a long event says which chunks matched, as byte ranges into the stored text; a new operator route shows how any event was chunked.
  • Also: code-plane access and safety fixes, admin-console fixes, /v1/answer abstention detection, usage-ledger fixes, new operator routes and metrics, and the client versions released with 0.10.1: cortexdbai 0.12.0 (PyPI) and 0.10.0 (npm), cortexdb-connectors 0.2.22, cortexdb-mcp 0.8.0 and cortexdb-cli 0.6.0 (see "Clients").

Upgrade from v0.9.13

Before you upgrade: what existing clients and operators will notice

Read this list even if you only use English. Several contracts change by default.

  • Gate new request fields on capabilities[]. A server that does not list a capability refuses its fields with 422 INVALID_BODY: v0.9.13 refuses each one as an unknown field (it has no capabilities key at all, which means none of them), and 0.10.1 names the fields in details.unsupported_fields and the missing names in details.required_capabilities.

  • Text blobs that are not valid text are refused. A text blob with no charset= whose bytes are neither UTF-8 nor UTF-16 with a byte-order mark now fails the write with 422 BLOB_TEXT_ENCODING_UNKNOWN instead of being stored as U+FFFD replacement text. In /v1/experience/bulk one such blob fails the whole batch. Declare the charset on upload (Content-Type: text/plain; charset=windows-1252) and it decodes.

  • /v1/recall context_block bytes change, English included, in four cases:

    • an assistant turn's echo of the user turn (In reply to user: ...) is dropped when that user turn is rendered in the same context;
    • an event too large for the remaining budget is skipped and packing continues with the smaller events behind it, instead of the context ending there;
    • no entry takes the block past budgets.max_tokens any more, the first one included: v0.9.13 admitted the first-ranked event whole whatever its size (a 32 KB answer recalled at max_tokens: 1000 came back as a 32,803-byte block);
    • an event that did not fit whole is served as an excerpt of its best-matching chunks in the room still free at its rank position.

    Otherwise the English bytes of context_block are unchanged: a pack of English events in which every event fits whole and no assistant turn echoes a rendered user turn is the same byte for byte. (A non-English event with a capture time zone gains a local-time bracket after its UTC one.)

  • layers.events[].content.text can be an excerpt. An excerpted event's item carries the same excerpt, flagged as a partial view: content._partial: true, content._partial_reason: "budget_excerpt", content._original_bytes and content._excerpt_ranges. Treat content.text as the whole text only when _partial is absent; GET /v1/events/{id} returns the whole event.

  • The provenance trail names every event the block renders. When the cross-layer knapsack evicts a rendered event from layers.events, its context_contributors row carries evicted_from_layers: true, and that row is the only place the response names it. Check forget, erasure and citation logic against the union of layers.events and those rows.

  • Packs that v0.9.13 returned empty may now answer, with a warning. When the query names an entity no retrieved event mentions, an all-English pack over an all-English scope is still emptied. The pack is now kept, with a warnings[] entry and a diagnostics.filters_applied note, when the query and the memories are in different scripts, the request carries a non-English hint or variant, or the scope holds at least two non-English memories.

  • /v1/forget reports only what it removed. It returns a top-level purge object (the shape /v1/erasures reports as summary.purge); a repeat forget returns zeros, a disabled graph adds no phantom document, and a scope-wide forget of 300 events reports 300, not 600.

  • Forget warnings name the real reason. The "use cascade=redact_events" warning fires only for the whole-scope derived_only shape. A memory_ids request whose layers leave out events gets "layers did not name "events" ..." instead; no selector request is told to use redact_events any more (dropping the selector would have turned a one-id forget into a scope wipe).

  • A forget or erasure has one cutoff. Everything the server stamped before it is removed and everything after it is kept. A capture sent at the same instant as a forget-all can land on either side; send captures you need to keep after the forget returns.

  • New retriable errors around destructive work: 409 DESTRUCTIVE_OPERATION_IN_PROGRESS on captures that would register a new scope inside a running forget or erasure (it used to be a permanent-looking 403 POLICY_DENIED), 409 SCOPE_STATE_CHANGED on scope edits there, 503 SERVER_SHUTTING_DOWN on forgets and erasures during a graceful stop, and 409 REDERIVE_IN_PROGRESS while a repair job runs. Retry them after Retry-After. The full list is under "Error codes".

  • Forgetting an event id that belongs to another scope answers 200 with every count 0, like a missing id, instead of 502 FORGET_BACKEND_FAILED (which revealed that the id exists elsewhere).

  • ?wait=captured on an inline or transcript blob write behaves like wait=indexed: the write keeps its blob lease until the event's link to the blob is published.

  • Non-Latin names get new entity ids. A name with any non-ASCII character is now ent_<ascii-base>-<16 hex>, or ent_u-<16 hex> with no ASCII letters. Pure-ASCII names keep their ids byte for byte, and ids stored by v0.9.13 (ent_Jos_) still resolve on reads and forgets.

  • Enrichment-on deployments get new derived output and spend. Facts, beliefs, belief ids and the extraction prompt change (see "Facts and beliefs"); the prompt is about 23% larger per English extraction call, plus one short model call per doubted polarity.

  • Give the server 120 s to stop. A graceful stop waits for running forgets and erasures (up to 60 s), persists, then drains the erasure residual pass (up to 20 s). Docker's default 10 s cuts that short: use docker stop -t 120, stop_grace_period: 120s (both compose files in this repository set it) or TimeoutStopSec=120 in a systemd unit.

  • Code plane (CORTEX_CODE_PLANE, off by default). A repository bound under a scope no roster governs is no longer served to per-actor tokens (404, left out of listings) until that scope is registered; a per-actor token can no longer register a repository without a governed scope; every registered repository is re-indexed once on the first start.

  • Admin console and telemetry figures move. events_stored counts each memory once, so it halves on unified-log stores; not-configured 503s no longer count in errors_total; a console restart exits with code 75; streamed answers now book their input tokens in the usage ledger, so the answer lane's recorded cost for streamed answers goes up (it was undercounted).

  • directives.extract: ["beliefs"] or ["understanding"] now run enrichment. They used to switch the enrichment lane off, discarding the work they asked for. extract: [] still turns enrichment off for the event. (The directive is honored on /v1/experience/bulk only; see "Known issues".)

Plan the first start

A v0.9.13 data dir needs no migration step: 0.10.1 opens it and does the one-time work in the background.

  • Erasure index backfill. The first start files every existing event-log row into the new erasure index, in the background and resumable. Measured: 6.6-7.1 s for a 1M-event store and 53 s for 5M, both overlapped with the start, whose ready time did not move beyond noise. Until erasure_index.wal.ready is true on GET /v1/admin/metrics, erasures read the whole store as before. Every start also rebuilds the erasure retry candidates and the idempotency reverse index in the background.
  • Belief sweep and reconcile (incremental-layer deployments, CORTEX_V1_LAYERS_INCREMENTAL). The first start removes builder beliefs whose topic key moved and rebuilds them under the new keys. The rebuild runs in the auto-layer ticks, at most 2,000 topics per scope per tick with one embedding call per belief (about 5 minutes for 1,400 topics in our tests), so swept beliefs are missing until it finishes. It is finished when GET /v1/admin/layers/stats reports belief_reconcile.done: true (dirty_topics counts what is still queued). done needs CORTEX_V1_LAYERS_AUTO on (without it nothing is seeded or swept: rebuild with POST /v1/beliefs/build); it never turns true while a CORTEX_V1_LAYERS_AUTO_EXCLUDE_SCOPES prefix covers a scope with facts (build those scopes by hand); and a failed build drops its topics, so compare by_scope.<scope>.belief_errors_total before and after. Deployments without incremental layers keep their old beliefs until a POST /v1/beliefs/build of each scope rebuilds them under the new keys. Beliefs you declared with POST /v1/beliefs are never swept.
  • Code plane re-index (only with CORTEX_CODE_PLANE on): every registered repository re-indexes once (extractor generation 4).

Upgrade steps

  1. Read "Before you upgrade" above, "Known issues" and "Rollback" below, and the "Erasure raw residual", "Erasure index" and "Answer context" sections of PRODUCTION_CONFIG.md.
  2. Enrichment-on deployments: run scripts/derived_corruption_audit.py against the running v0.9.13 server and keep the report. It is read-only, uses GET routes only, and is the input of the repair in step 7. Take it before the upgrade: after 0.10.1 rebuilds the beliefs, a colliding legacy entity id can stop showing up in a new audit.
  3. Rehearse on a copy if you can: restore a cold backup of the data dir on a separate host, start 0.10.1 on it with the production environment, and time the first start (the reconcile above is the slow part). Never start a second server on a production data dir, and never build the binary on a production host.
  4. Record exactly which build you deploy (image digest or binary sha256, and the git_sha that GET /v1/admin/version reports), and keep the current v0.9.13 artifact for a rollback.
  5. Pause ingestion, stop v0.9.13 with a grace of at least 120 s, and take a cold backup (scripts/cold_backup.py). If you keep the old container for a rollback, set its restart policy to no (docker update --restart=no <container>): under always, a Docker daemon restart starts a stopped container again, and two builds would race for the same port and data dir.
  6. Start 0.10.1 with a 120 s stop grace, wait for GET /v1/admin/ready, and on incremental-layer deployments for belief_reconcile.done. Check that capabilities[] lists the ten names under "Capabilities".
  7. Enrichment-on deployments: follow docs/DERIVED_REDERIVE_RUNBOOK.md with the audit report of step 2 (see "Repairing derived data written by earlier builds"). An apply there is one-way for a rollback to v0.9.13, so decide on the rollback window first.
  8. For each billing tenant that uploaded text blobs, run POST /v1/admin/index-audit/source-blob-links (dry run first), then review GET /v1/admin/index-audit/orphan-blobs (see "Erasure").
  9. Resume ingestion. Update clients to gate the new fields on capabilities[].

If you ran a pre-release 0.10.0 or multilingual branch image

Replace the binary or image; nothing is migrated beyond what the first start of a 0.10 build already did. With enrichment on, the pre-release builds could store a negation twice, drop a predicate's negation, or supersede a fact with one about another subject (fixed in 0.10.1, see "Extraction fixes in 0.10.1"). Run scripts/derived_corruption_audit.py (tool version 2 reports negation_doubled and negation_dropped) and repair what it lists with the re-derivation route. Recall contexts differ from the pre-release builds wherever an event is longer than the room it gets.

Capabilities

GET /v1/admin/version returns {version, crate_version, git_sha, built_at, capabilities}. capabilities is a sorted, append-only list of API-shape names; a breaking change would ship as a new _v2 name beside the old one, never by changing a listed one. 0.10.1 lists ten:

CapabilityWhat it addsRequest fieldsResponse fields
blob_text_decode_v1Charset-safe decoding of text blobsnone (behaviour)422 BLOB_TEXT_ENCODING_UNKNOWN
budget_excerpt_v1No context entry exceeds the budget; oversized events served as excerpts; the context receiptnonelayers.events[].content._partial_reason: "budget_excerpt", _original_bytes, _excerpt_ranges; context_contributors rows with evicted_from_layers
capture_language_v1Language, script and time-zone hints on captureslanguage, scripts[], context.timezone on /v1/experience and /bulklanguage, scripts, language_source, context.timezone on event reads and recall items
erasure_blob_cascade_v1Erasure deletes the source blob; the raw residual passnonesource_blob_ids on events; purge on /v1/forget; purge.blobs, purge.retained_shared_blobs, log_residual_cleared_at, log_residual_pending
matched_fragments_v1Which chunks of a long event matchednonelayers.events[].matched_fragments[]; the context_fragments trail entry
query_variants_v1Extra phrasings of the questionquery_variants[] on /v1/recall and /v1/recall/streamper-variant ranks in the diagnostics side table
recall_diagnostics_v1Per-channel recall diagnosticsdiagnostics: "full" (existing field)diagnostics.items, drops, channel_lists, items_truncated
recall_language_v1Language and script hints on recalllanguage, scripts[] on /v1/recall and /v1/recall/streampack warnings[]
refers_to_v1The times a memory talks aboutcontext.refers_to[] on captures; temporal.refers_during on recall and answercontext.refers_to on events
temporal_lenient_v1Lenient time phrases and calendar time zonestemporal.natural_mode, temporal.timezone on recall and answerthe temporal_natural_ignored warning

fact_surface_v1 is known to the server but not listed: the fact fields it will promise (below) are returned already, but do not depend on them until the name appears.

Multilingual capture and recall

Nothing is translated. You send the original text plus hints; the server keeps both and never rewrites the text.

Capture tags (capture_language_v1)

  • Fields. language (BCP-47, for example hi-Latn), scripts[] (ISO 15924, at most 8) and context.timezone (an IANA zone name) on /v1/experience and each /v1/experience/bulk item. Values are canonicalized (HI-latn becomes hi-Latn, deva becomes Deva, duplicate scripts are dropped). A malformed value is 422 INVALID_ENVELOPE. timezone must be an exact IANA name from the bundled tzdata list (canonical zones and backward links): IST, +05:30 and Asia/Mumbai are refused.
  • Every event gets one language tag, decided in this order: your language (and scripts), then a non-English source.language, then a lang:<tag> label (for example lang:hi-Latn), then script detection (und-Deva, und-Arab, ...; Tamil, Telugu and Thai map to ta, te and th), then a Latin-script Hinglish heuristic (hi-Latn), then English. Mixed text lists every script, the dominant one first; digits, punctuation, currency signs and emoji add no script.
  • What is stored and returned. Non-English events return language, scripts and language_source (hint, source, label or detected) on GET /v1/events/{id}, recall layers.events items and /v1/export. An event that is English by default stores no tag, so English events are byte-identical to v0.9.13; an explicit "language": "en" is stored.
  • Not part of write identity. The tags, the time zone and refers_to are excluded from the idempotency identity, so the same body with or without hints replays with no 409. A replay whose hints differ keeps the first write's tags and returns warnings: ["conflicting_hint_on_replay: ..."] on the 202, on the wait 200 and on each bulk results[] item.
  • Erasure clears context.timezone and context.refers_to; the language tag stays, because it classifies the erased text without quoting it.

Recall hints and stoplists (recall_language_v1)

  • language and scripts[] on /v1/recall and /v1/recall/stream (validated like the capture hints; malformed values are 422 INVALID_BODY with details.field).
  • They select the query stoplist used by BM25 and the lexical re-ranking stages: English with no hint and with an en, hi or hi-Latn hint (a Hinglish list measured a recall loss); nothing stripped for fil, id, vi, und- tags, every other language, or a scripts hint without Latn. The query text alone never changes the stoplist.
  • /v1/answer does not take language or scripts.

Advisory entity grounding

When the query names an entity (a capitalized or quoted anchor) that no retrieved event mentions, the grounding gate used to empty the pack. It still does for English over an all-English scope. It now keeps the pack and adds a warning when any of these holds, checked in this order: non_en_hint (the recall language is not English), non_en_variant (a query variant's is not), script_mismatch (a retrieved event has a script the query, its variants and the scripts hint lack), scope_census (the scope subtree holds at least two distinct non-English memories). The pack carries warnings: ["entity_grounding_advisory: query entities [...] not found in retrieved memories; pack kept (<reason>) - results may be about a different entity"], and diagnostics.filters_applied gets entity_grounding:advisory:<reason> under summary and full. The census counts live captures at once and folds history in with a bounded background scan per scope, so right after a restart the first recall of such a scope may still be enforced.

Query variants (query_variants_v1)

  • query_variants[] on /v1/recall and /v1/recall/stream: at most 4 entries of {text, language?, script?}, each at most 4,096 bytes. More entries, empty text or longer text is 422 INVALID_BODY with details {field, max_items, max_text_bytes}. A variant that repeats the query or an earlier variant is dropped silently.
  • Variants are embedded in the same provider call as the question and each runs its own vector and BM25 lists, merged with the primary results round-robin (primary first). Reranking still scores against the question only. A variant can rescue a grounding miss but never cause one.
  • Cost: about +100 ms at p50 with two variants on text-embedding-3-small, one provider round trip.
  • CORTEX_QUERY_VARIANTS_FUSION (interleave default, depth_cap) and CORTEX_QUERY_VARIANTS_DEPTH are for evaluation; deep_rrf is a negative control, never a production setting.

Time: lenient phrases, calendar zones and referenced times (temporal_lenient_v1, refers_to_v1)

  • temporal.natural_mode on /v1/recall, /v1/recall/stream and /v1/answer: strict (the default) keeps the 422 UNPARSEABLE_TEMPORAL for a phrase the server cannot parse; lenient drops the phrase, applies no time filter, and adds temporal_natural_ignored: unparseable temporal phrase "..." ignored (natural_mode=lenient); no time filter was applied to the pack's warnings[].
  • temporal.timezone (IANA): calendar phrases such as today, yesterday, this week and last month resolve on that zone's calendar. Without it the result is exactly the UTC one.
  • context.refers_to[] on captures: the times the memory talks about, as local dates (YYYY-MM-DD) or date-times (YYYY-MM-DDTHH:MM[:SS], no offset) in from, to or candidates[], with a zone (else context.timezone) and a precision (minute, hour, day, week, month or year). An ambiguous day word ("kal" is yesterday or tomorrow) keeps every reading in candidates. Invalid entries are 422 INVALID_ENVELOPE.
  • temporal.refers_during {from, to, zone?} on recall and answer is a boost, not a filter: the retrieved memories whose refers_to (or, without one, capture time) falls in the window move to the front; nothing is added or removed. Date bounds compare calendar dates in each memory's own zone; a bound with a time compares instants. A malformed window is 422 INVALID_BODY (details.field: "temporal.refers_during"). The trail records the boost (refers_during_boost), and on a small scope whose pack is the whole corpus in time order the order is left alone.
  • temporal.valid_during is documented as what it always was: a filter on capture time (observed_at).
  • A non-English event with a capture context.timezone gets a local-time bracket after the UTC one in its context prefix ([2026-09-23 20:30 UTC] [2026/09/24 (Thu) 02:00 Asia/Kolkata] ...). English bytes are unchanged.
  • /v1/answer does not return the pack warnings[], so a lenient note is not visible there.

Per-channel recall diagnostics (recall_diagnostics_v1)

diagnostics: "full" adds a side table: items[] (each returned event, then the ranked tail, with its final and fused rank, its rank and score in every channel (vector, BM25, fact, semantic fact), its rank in every variant, matched_via and reorder deltas), drops[], channel_lists and items_truncated (at most 200 rows). summary and full still need diagnostics.read, and none still serializes as {"time_ms":{}}. The layers, context_block and citations are the same at every level.

Charset-safe text blobs (blob_text_decode_v1)

When a capture references a text blob, the bytes are decoded in this order: a UTF-16 byte-order mark (stripped) or a UTF-8 one (kept, so existing blobs keep their write identity); a declared charset=utf-16, utf-16le or utf-16be; strict UTF-8; then the declared charset= label. Anything else fails the write with 422 BLOB_TEXT_ENCODING_UNKNOWN (retriable: false, details {blob_id, content_type, reason, charset}); no path stores U+FFFD for bytes that did not encode it. A UTF-32 byte-order mark, ISCII labels and a declared charset=utf-8 over invalid bytes are refused. POST /v1/blobs itself is unchanged.

Entity ids and enrichment of non-Latin text

  • Entity ids of names with any non-ASCII character are ent_<ascii-base>-<16 hex> (for example ent_jose-...) or ent_u-<16 hex>, a hash of the normalized name (NFC, lowercase, invisible format and direction marks removed; Arabic diacritics too). v0.9.13 replaced every such character with _, so different people in a non-Latin script shared one id (ent_______).
  • Ids stored before (ent_Jos_) still resolve on GET /v1/beliefs?about=, the fact ledger and /v1/forget. A legacy id that names more than one person resolves to nothing on a forget (logged forget_entity_id_ambiguous), so forgetting one person never deletes another's facts; on ?about= it returns every belief stored under it.
  • CORTEX_ENRICHMENT_SKIP_NON_EN=1 (opt-in) skips LLM enrichment for text containing letters of a non-Latin script; such an event stays searchable as a raw event. Latin-script Hinglish is always enriched.
  • A triple whose predicate normalizes to nothing or to non-ASCII is rejected and counted, never stored with an empty key.
  • Counters: cortexdb_enrichment_non_en_script_events_enriched_total, cortexdb_enrichment_non_en_script_events_skipped_total and cortexdb_enrichment_triples_rejected_total{code=...} on /metrics, and the same values under enrichment_multilingual on GET /v1/admin/metrics.

Documents: matched fragments and chunk inspection

  • matched_fragments (matched_fragments_v1). The write path splits an event longer than one chunk (about 500 bytes) into overlapping chunks. Each chunk now records where its text lies in the stored text, and a recall item of a multi-chunk text event lists matched_fragments[]: the chunks a retrieval channel returned for this recall, best first, at most 8, each {chunk_id, text_source, byte_range, locator?, language?, matched_via}. byte_range re-slices the event's stored content.text (UTF-8 bytes); locator is the section path; text_source is event_text or converted_text (a text blob the server decoded); matched_via is vector or bm25. Order is the signal; later entries can be any chunk a channel returned. The context_fragments provenance entry names the chunks context_block served.
  • Events written before 0.10.1 have no fragments until they are rewritten or re-derived by an index-audit repair; their items carry no matched_fragments.
  • GET /v1/admin/events/{id}/chunks (trusted operator plus diagnostics.read, confined to the caller's tenant and the event's scope; an unknown, foreign or erased id is the same 404) shows how one event was chunked: each chunk's id, its index text (exactly what BM25 and the vector were built from), sizes, whether it has a vector, and its fragment (stored with the byte range, implicit for a single-chunk event, or none for an event written before).
  • CORTEX_CHUNK_FRAGMENTS=0 stops writing fragment rows (default on). Rows hold offsets only, never text, and are erased with their chunk.
  • Chunking itself is unchanged: the chunk bodies that are indexed are the same bytes as before, and a single-chunk event gets no fragment row and no response field.

Recall and answer context

Answer-context packing restored (the whole-event context fix)

Since v0.9.10 context_block is built from whole events, and the /v1/answer context packer had not been adjusted to that: derived fact rows behind a timestamp were ranked as structure and cut first, assistant replies carrying an echo of the user turn were counted as user evidence, and one oversized event ended the context. 0.10.1 fixes all three. Dialogue with named speakers ("Caroline: ...") ranks level with fact rows, and in a congested multi-topic answer each topic's focused dialogue and fact rows are packed before the rest. Each fix has a switch:

SwitchDefaultEffectPre-fix value
CORTEX_EVIDENCE_ROLE_CLASSIFIERpackerTimestamp- and role-aware evidence classifier for the answer packer and the multi-session preamble (full: every consumer)legacy
CORTEX_CONTEXT_STRIP_ECHOpairedDrop an assistant turn's In reply to user: echo when the quoted user turn is rendered in the same context (all: always)0
CORTEX_CONTEXT_RENDER_SKIP_OVERSIZEDonSkip an event that does not fit and keep packing (affects every ranked /v1/recall)0
CORTEX_FACET_PACK_POLICYtieredIn a byte-congested merge of the primary and facet recalls, focused dialogue and fact rows firstlegacy
CORTEX_CONTEXT_ASSISTANT_EXCERPTSoffOpt-in: long assistant turns render only their matched chunk spans (CORTEX_CONTEXT_ASSISTANT_EXCERPT_MAX_BYTES, default 600)0
CORTEX_ABSTAIN_SELF_RESCUElegacy passThe abstention self-rescue; guarded (opt-in) skips it when the abstention denies the question's premise; 0 never rescuesunset
CORTEX_RECALL_BUDGET_EXCERPTSonNew in 0.10.1: budget excerpts, below0

Setting every value in the last column restores v0.9.13's packing for these stages (PRODUCTION_CONFIG.md, "Answer context", has the line to paste).

Budget excerpts (budget_excerpt_v1, new in 0.10.1)

The render places whole events in rank order while they fit, as before, and skips one that does not. Then each event it left out gets the room still free at its rank position: whole, if an echo strip freed enough room since, otherwise as an excerpt.

  • The excerpt is built from the event's best-matching chunks (the fragments behind matched_fragments, plus any chunk a channel returned), taken best first while they fit and served in document order. An event with no fragment rows (written before 0.10.1) gets its head, cut at a character boundary.
  • The slices are verbatim text joined by \n[…]\n, led by […]\n when text before the first slice is left out and closed by \n[…] when text after the last one is. Echo stripping does not apply inside an excerpt.
  • An excerpt needs at least 512 bytes of room unless nothing else was placed.
  • The excerpt is the same text in context_block and in the event's layers.events item, whose content keeps its kind and role and becomes a partial view:
"content": {
  "kind": "message",
  "role": "assistant",
  "text": "[…]\nSection 3: …\n[…]\nSection 40: …\n[…]",
  "_partial": true,
  "_partial_reason": "budget_excerpt",
  "_original_bytes": 32768,
  "_excerpt_ranges": [{"start": 775, "end": 1015}, {"start": 9692, "end": 9932}]
}
  • _original_bytes is the stored text's byte length; _excerpt_ranges are the half-open byte ranges of the stored text the excerpt carries, in document order. The knapsack charges the excerpt, not the whole event. matched_fragments byte ranges keep indexing the stored text, and context_fragments names the chunks the excerpt's exact ranges cover. The stored event is untouched.
  • The holistic /v1/recall rendering (the # Memory context wrapper) cuts its first evidence entry to the room its synthesized sections leave, closed by \n[…], and reports the coverage as partial.
  • For the whole text, read GET /v1/events/{id} or raise budgets.max_tokens. The default recall budget is 4,000 tokens (about 14,000 bytes of context; CORTEX_RECALL_DEFAULT_MAX_TOKENS changes it).

Measured on test shapes (deterministic mock embeddings, the build before this change against 0.10.1):

ShapeBefore0.10.1
One 8 KB document, max_tokens: 2000 (7,000-byte budget)8,227-byte block, empty events layer6,874 bytes; a 6,839-byte excerpt in the layer
One 32 KB document, max_tokens: 100032,803 bytes (9.4 times the budget)3,493 bytes
Mixed pack whose 8 KB answer did not fit, max_tokens: 2000188 bytes, answer missing6,620 bytes, answer as a 6,390-byte excerpt
One 9 KB Devanagari document, max_tokens: 20009,303 bytes6,943 bytes

With CORTEX_RECALL_BUDGET_EXCERPTS=0, 23 of 23 recalls over one store were byte-identical to the build before; with the default, the 11 that differed were exactly the packs with an oversized event and at least 512 bytes of room.

Every event in the context is identifiable (the context receipt, new in 0.10.1)

  • The cross-layer knapsack now evicts the events context_block does not render before any it renders. When the render is a rank prefix this is the same as before; when an oversized event was skipped mid-list, that event goes before the smaller rendered ones behind it.
  • The provenance entry {"phase": "context_contributors", "context_contributors": [{"event_id", "rank"}, ...]} names the events context_block renders, after the ranked candidates and any recovered user turn. A rendered event the knapsack evicted from layers.events carries evicted_from_layers: true. Those rows are outside the 128-row cap, one per evicted event, so an exhaustive or order: "recency" block with a small explicit max_tokens can carry hundreds.
  • rank has two meanings in one list: on the rows v0.9.13 already wrote (the ranked candidates, recovered_user_turn) it is the retrieval rank; on the added rows it is the event's position in the events layer before the knapsack.
  • On a 12-turn test conversation with one 10.6 KB reply at max_tokens: 2000, the build before this change left 2 to 10 of the block's events (depending on the query) unnamed anywhere in the response; 0.10.1 names all of them.
  • budgets.max_tokens (API reference §8.3 and §9.2) is documented as the code applies it: layers named in include are funded first, in include order, then facts, beliefs, episodes, understanding and events, and within the events layer the events the block does not render leave first. CORTEX_CROSS_LAYER_KNAPSACK=0 turns the knapsack off.

/v1/answer merges excerpts across its recall passes (new in 0.10.1)

/v1/answer merges the contexts of several recall passes run at different budgets (primary, supplemental, adaptive, fact-join and facet passes), so one pass can serve an event as an excerpt while another serves it whole. The answer context treats the two as one event: the whole text is kept once, in the excerpt's place when the excerpt came first, and the excerpt is dropped. Excerpts of the composer and of CORTEX_CONTEXT_ASSISTANT_EXCERPTS, which carry the same […] mark, are not budget excerpts and merge as before.

/v1/answer and /v1/compose diagnostics and abstentions

  • diagnostics.degraded_sources names what the answer ran without, as stable codes only, never error text: coordinator_recall, wal_supplement, derived_<lane> (for example derived_facts), artifact_search, index_divergence, derived_backfill, facet_plan_unavailable, facet_recall_failed, ms_executor_fallback, else unknown. The answer is still returned; read a non-empty list as "this answer saw less evidence than a healthy run would". The error stays in the server log.
  • "No such information" answers are detected as abstentions; general nouns ("issue", "problem") are no longer treated as unsupported premises. New diagnostics: premise_unsupported_terms (question words with no literal support in the context) and extractive_retry_outcome (answered, still_abstained or premise_ungrounded).
  • A /v1/answer request carrying the evaluation-only facet_plan_override is refused with 422 EVAL_OVERRIDE_DISABLED unless the server runs with CORTEX_EVAL_FACET_PLAN_OVERRIDE=1. Never set a CORTEX_EVAL_ variable on a production server.

Regression checks

These are regression checks with answer-free replays and a blind benchmark harness, not published scores.

  • LongMemEval-S, full 500 questions, blind server harness, enrichment off: the multilingual work passed a non-inferiority check against our previous baseline run under the same configuration. That run predates the /v1/answer abstention changes above and 0.10.1's context changes; the context changes were checked with the replays below.
  • 0.10.1's context changes, answer-free replays (embeddings only, no answer model) against the build before them: on LongMemEval-S (all 500) the contexts hold the same gold evidence in every question type (a repeat run moved single turns both ways, the replay's own noise), and 21 of the 500 contexts carry an excerpt, none next to its own whole text. On a conversation store with one event per session, where excerpts fire most, the contexts hold more gold turns than before in every question category (most of the added turns are inside excerpts); six turns that the previous contexts reached only in their last, already truncated session no longer fit (see "Known issues"). With CORTEX_RECALL_BUDGET_EXCERPTS=0 the contexts are the previous ones.

Facts and beliefs (enrichment-on deployments)

Nothing in this section runs unless enrichment is on (CORTEX_ENRICHMENT_DELAY_SECONDS set). With enrichment off (the default) there are no facts or beliefs, and English recall packs are byte-identical to v0.9.13's by our regression gate. The event log is never rewritten: every change below is derived output.

Negation, cessation and contradiction

  • Extraction marks every claim affirmative, negated or ceased (stored as the fact's polarity), with a deterministic English fallback (not, never, n't, no longer, ... anymore) for claims the model left untagged.
  • A denial never retires the claim it denies. An affirmative and a denial of the same claim are a contradiction: neither supersedes, one durable conflict row is opened with both facts as members, and the belief is Contradicted with the denial counted Against.
  • A denial of another value never retires anything, on any relation: "Meera doesn't eat non-veg" no longer supersedes "Meera eats fish", and works_at | not Globex no longer retires works_at | Acme.
  • A cessation is an update. "Priya no longer lives in Delhi", "doesn't work at Acme anymore" and "isn't working there any more" supersede the earlier claim with no conflict; a claim made again after its cessation supersedes the cessation.
  • A restated value supersedes nothing. Two facts stating the same value are two Supports.
  • Do-support denials fold to the affirmative predicate (did_not_go_to groups with went_to).
  • A claim whose polarity is in doubt gets one short verifier call (below).

One subject per supersession

  • The consolidator no longer merges facts about different people (Priya / Riya, Amit / Amita): duplicates, stale and resolved verdicts between facts whose named entities differ are dropped (code=consolidation_guard_names).
  • Neither the write-time resolver nor the consolidator retires a fact with one about another subject: two triples need the same subject (letter case and a leading article aside), and two sentence facts that lead with different names ("The service centre in Chennai is open ..." / "... in Hyderabad ...") are two subjects however alike the rest.
  • This changes updates stated only as sentences: a sentence whose subject is not a name ("User lives in Boston", "I ...", "The order ships from Pune") and whose value is a proper noun no longer supersedes the earlier sentence; both stay live. An update extracted as a triple (lives_in | Denver) still supersedes.

Stable belief keys and deterministic ids

  • Facts and beliefs share one topic key, (subject, predicate), used by POST /v1/beliefs/build, the incremental builder, the fact store and the resolver alike. Subject and predicate go through a versioned Unicode fold (NFC, lowercase; on ASCII exactly the old key); the predicate is the built-in ontology's canonical form, so employed at groups with works_at. intends_to and the future forms become plans_to, the diet variants diet, flight_at flight_time.
  • Every fact-derived belief id is derived from (scope, topic): the same facts in any order give the same beliefs and ids. POST /v1/beliefs/build used to mint new random ids on every call.
  • Beliefs carry claim_key beside claim ({subject, predicate, value, polarity, rules_version}), the form the belief groups and compares on. claim keeps the latest supporting fact's own words.
  • A multi-valued relation (likes, owns, plans_to, ...) gets one belief per value; synonyms of a multi-valued relation ("likes tea", "enjoys hiking") never supersede each other.
  • Place exonyms are one value (Dilli/Delhi, Bombay/Mumbai, Bangalore/Bengaluru, Calcutta/Kolkata, Madras/Chennai), for comparison only; the stored text is never rewritten.
  • /v1/conflicts items carry confidence: a value_disagreement whose values are within edit distance 2 of each other ("Jon" / "John") opens at 0.5. A later stated update of an observed value is a correction, not a temporal_overlap conflict.
  • The per-person disposition_profile rollup needs two distinct dispositions.
  • A cross-language restatement stays two Supports: a consolidator duplicates verdict between a Hinglish and an English fact is not applied (code=consolidation_cross_language_kept).
  • API reference §7.4.2 has every rule.

Extraction reads the original text

  • Original wording. Facts keep subjects, objects and text in the source script; predicates are English snake_case. Every triple and fact asks for a quote, kept only when it is a byte-exact span of the capture's own text (quotes_rejected_not_in_source counts the rest).
  • English glosses are keys only. A non-English fact gets a derived English text_en / object_en gloss, stored in the fact's metadata and as one key-only search row that recall resolves to the fact. The gloss never appears in context_block, citations or layers.events, and erasing the source removes it.
  • Dates. The extraction's date anchor is the capture's observed_at (local wall time when the capture named a context.timezone), not the write time. A caller's refers_to entry is the date for its phrase. Each dated triple records date_basis (refers_to, verbatim, extractor or ambiguous); an ambiguous day keeps every reading in date_candidates and stamps neither on the fact.
  • Polarity re-check. A claim whose polarity a trigger doubts (a gloss that disagrees, a Hindi negation cue in its clause the model ignored, an English quote that may deny it, a negated antonym such as a negated dislikes) gets one re-check call to the enrichment model, whose verdict is stored (polarity_check: rechecked). Without an answer the fact keeps the extractor's polarity (unchecked), or, for a gloss mismatch, is stored at confidence 0.6 (unresolved) so it cannot retire a confident fact.
  • Fact fields. Facts on recall layers.facts and /v1/facts may carry language, polarity, quote, date_basis, date_candidates and gloss {text_en?, object_en?, derived: true, model?, prompt_version?}. These are additive and optional; fact_surface_v1, which will promise them, is not listed yet.
  • CORTEX_ENRICHMENT_SCOPE_QUIET_MS (opt-in, default off) holds a busy scope's captures until the scope has been quiet that long (at most CORTEX_ENRICHMENT_SCOPE_QUIET_MAX_MS, default 30 s), so close captures share one extraction call, at the cost of capture-to-fact latency.

Extraction fixes in 0.10.1

Upgrade drills of the 0.10.0 candidate on enrichment-on test stores found three defects in the facts it wrote. They are fixed in 0.10.1:

  • A negation stored twice. A denial the model wrote with an antonym ("does not want SMS updates any more" as dislikes | SMS updates, negated) is folded back to the claim it denies (wants | SMS updates, negated and ceased), including when it is phrased as a cessation, when an order number or a name puts object words in another clause, and for denial verbs the ontology does not map (rejects, refuses, declines, hates, lacks). "can't stand coffee" is a dislike. A negated triple whose object states its own denial ("wants | no delivery on Sunday") or an exclusion ("all purchases except gift card purchases") gets one verifier re-check, whose verdict is stored. An opener followed by a digit, the numero "No." or a capitalized name ("No. 45, 2nd Cross", "No Cost EMI", "Nothing Phone 2a") is not a denial at all, and a negated claim that also ceased ("I no longer live at No. 45") is never sent to that re-check: it keeps its negation and its cessation.
  • A predicate's negation dropped. An "affirmative" verdict on a predicate that carries its own negation (not_arrived | true for "my order has not arrived yet") is stored negated; idioms whose negation asserts ("can't wait", "never misses", "don't mind") are left alone.
  • Supersession across subjects. The resolver's free-text keys and the consolidator's name guard both require one subject (above).

On an enrichment-on test set of about 720 captures, facts with a doubled negation went from 8 to 0, with a dropped negation from 39 to 0, and supersessions across subjects from 37 to 0; the English regression slice and the Hinglish test set held. GET /v1/admin/metrics reports the triggers under enrichment_multilingual.fact_surface (polarity_object_denial, polarity_exclusion_denial, polarity_predicate_negation_kept, polarity_antonym_denial, polarity_recheck_settled, _unresolved, _unchecked, _flipped and others).

Other derived-layer changes

  • A single POST /v1/experience with content.kind: "triple" writes its fact, as bulk always did; ?wait=indexed returns after the fact exists. A stored triple is always treated as extract: [], so the LLM extractor never sees it.
  • directives.extract gate. Naming beliefs or understanding now runs the enrichment lane those stages depend on (they used to switch it off); naming only episodes still leaves it off, and extract: [] still denies every stage. The directive selects work, not output: a lane that runs writes everything it produces, so extract: ["entities"] still yields facts.
  • More derived rows are erased with their capture: the extraction summary row, the facts' question-key and gloss rows, each fact's own search rows (a forgotten fact's text could still be served before), and with CORTEX_ENTITY_GRAPH=1 the capture's graph and resolver rows.

Spend

The extraction prompt is about 23% larger per English knowledge call (3,564 against 2,916 prompt tokens measured), plus one extraction-model call per doubted polarity. CORTEX_POLARITY_RECHECK=0 records the doubt without the call; CORTEX_POLARITY_QUOTE_TRIGGER_EN=0 turns off only the English quote trigger. Every call counts against CORTEX_ENRICHMENT_MAX_LLM_CALLS_PER_EVENT. Price your models in CORTEX_MODEL_PRICES.

Erasure and forget

One cutoff for everything a request removes

A scope-wide forget and every /v1/erasures execute take one server cutoff and the event-log head, once, before reading any event, and every pass selects by it: the event rows, the scope tombstone, the content and raw-log pass, the artifact closure and the derived layers (a derived row built only from captures after the cutoff survives). A capture the server stamped after the cutoff is never removed. A capture stamped before it whose acknowledgement went out after it is history the forget selected, and is removed. ?wait=indexed no longer reports a row a concurrent forget blanked as 502 INDEXING_FAILED.

Honest counts and warnings

  • /v1/forget returns purge (events, documents, edges, blobs, retained_shared_blobs, twins_redacted, bitemporal_records_erased, wal_rows_redacted, artifacts_cascaded), and deleted.blobs / deleted.retained_shared_blobs when non-zero. Every count is a transition this request made; deleted.events on a scope-wide redact_events forget counts each event this request blanked, once.
  • POST /v1/forget/preview counts only events not yet blanked, counts entity-id and attribute selectors (about_entity, about_subject, predicate, ent_ ids) with the execute's own match, and returns requested and matched.
  • Forget by entity id: about_entity, about_subject and an ent_ id in memory_ids (under cascade: "derived_only") resolve the ids /v1/facts and /v1/beliefs show, inside the request scope and its descendants only.
  • The /v1/erasures preview's estimated_duration_ms is 250 ms plus 80 ms per event, a planning figure. summary.deleted_events is the pinned count.

Blobs (erasure_blob_cascade_v1)

  • An event made from an uploaded text blob, or a transcript sent in place of one, records the link (source_blob_ids, returned on /v1/events, recall items and export). Erasing the event deletes the blob bytes and retires its record unless another live event still links or references it; purge.blobs and purge.retained_shared_blobs report it, and the previews count the same blobs. Labels are never trusted as links.
  • Events written before 0.10.1 have no link. Run POST /v1/admin/index-audit/source-blob-links (trusted operator; dry run by default, which needs diagnostics.read; applying needs admin.compact; unified log only, else 409 UNIFIED_WAL_REQUIRED) once per billing tenant to backfill it from the source_blob: labels. It links a label only when it names a live blob of the event's own tenant and caller, and reports the rest as unverifiable. Then read GET /v1/admin/index-audit/orphan-blobs (read-only, ?limit=): the tenant's live blobs no surviving event links or references. A blob inlined before 0.10.1 and not backfilled is listed even while its event is live, so check each before deleting it with DELETE /v1/blobs/{id} (unchanged).

Bytes left in the files (the raw residual pass)

A delete in RocksDB or the full-text index only adds a newer version; the erased bytes stayed readable in log files, older SST files and index segments until the engines compacted them. After an erasure a throttled background pass now flushes the stores, compacts the erased keys out of the content store, the event log and the session fragments, and merges the full-text segments that still hold a deleted document. GET /v1/erasures/{id} reports log_residual_cleared_at when the passes covering the job finished, or, while that is null on a completed job, log_residual_pending with a reason:

reasonMeaning
pass_pendingThe pass has not finished the work this job recorded yet
fulltext_segment_over_budgetA full-text segment larger than the per-pass budget still holds documents erased under an overlapping scope; it clears only once no segment can still hold them, and on a store whose segments exceed the budget it may never clear (it never clears falsely)
not_coveredNo pass covers this job: CORTEX_ERASURE_RESIDUAL=0, or the legacy dual WAL (CORTEX_UNIFIED_WAL=0)

A graceful stop drains the pass for up to CORTEX_ERASURE_RESIDUAL_SHUTDOWN_SECS (20 s); what is left is journaled and resumed at the next start. CORTEX_ERASURE_PERIODIC_COMPACTION_SECS sets a periodic compaction backstop for rows a crash kept out of the pass. GET /v1/admin/metrics reports the pass under erasure_residual. docs/ERASURE_COVERAGE.md lists every store that holds text and what erasure reaches.

An erased fact stays erased after a crash

An erased fact id is recorded in a synced fence before it leaves memory, and every start drops fenced ids from the loaded fact snapshot, so a hard kill before the next snapshot save can no longer bring an erased fact (text included) back. A fence that cannot be read fails the start rather than serve erased facts.

Faster forgets

Redactions are written in batches with one sync per 256 rows, a forget makes one full-text commit, and discovery no longer decodes rows of other scopes. Captures into registered scopes, reads and health checks no longer wait behind a forget (a forget used to hold the scope registry while it ran). Measured forget-all of a 20-event scope under concurrent captures: median 167 ms, against 298 ms on v0.9.13; on a 100,000-event store 369 ms, against 2,471 ms on v0.9.12.

Erasure cost follows what is erased, not the store

A selective /v1/erasures, a /v1/forget by id (a repeat included) and a scope erasure no longer read the whole store. The raw-log discovery, the blob survival proof, the preceded_by / supports: scrub and the idempotency cleanup read an erasure index kept in the same write as each event-log row; a vector delete no longer rescans its whole index shard; and /v1/erasures deletes its rows in synced batches of 256. Measured on copies of one store under a background load of 20 captures/s and 10 recalls/s, the same release without the index against 0.10.1:

Request1M events, without1M, 0.10.15M events, without5M, 0.10.1
/v1/forget by id, large scope12.2-13.0 s0.04-0.37 s64.0-66.8 s0.09-0.27 s
the same forget repeated4.4 s6 ms23.4 s90 ms
/v1/forget, 100 ids14.0 s0.21 s71.2 s0.57 s
/v1/erasures, 1 id12.7-13.5 s0.05-0.12 s67.6-71.1 s0.05-0.07 s
/v1/erasures, 1,000 ids25.2 s0.64 s105.0 s2.3 s
/v1/erasures, 1 blob-backed id27.7-27.9 s0.06-0.14 s137.5-140.1 s0.07-0.11 s
/v1/forget scope-wide, small scope21.8-22.0 s0.44-0.47 s103.5-179.5 s2.5-2.6 s
/v1/erasures whole scope, small scope33.3 s0.56 s175.3 s2.8 s

The first start backfills the index (see "Plan the first start"); until erasure_index.wal.ready is true on GET /v1/admin/metrics, erasures read the whole store as before. What still grows with the store: a scope erasure's pass over the content store (0.28 s at 1M events, 1.4 s at 5M), the bi-temporal closure when bi-temporal records are erased, and, on the legacy dual WAL, every scan the index replaced. CORTEX_ERASURE_INDEX=0 keeps the index maintained but unread (an A/B switch and escape hatch); CORTEX_ERASURE_INDEX_BACKFILL=0 skips the backfill. docs/ERASURE_COVERAGE.md ("What an erasure reads") has the full table.

Concurrency and shutdown

  • While a forget or erasure runs it holds a lease on its scope footprint. A capture that would register a new scope inside it waits up to CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS (default 120) and then gets 409 DESTRUCTIVE_OPERATION_IN_PROGRESS (Retry-After: 5, details {scopes, retry_after_seconds, admission_wait_seconds}); nothing is written. /v1/scopes register, member edit and delete inside the footprint get 409 SCOPE_STATE_CHANGED. Captures into existing scopes and everything outside the footprint never wait.
  • A forget or erasure runs detached from the HTTP request, so a client disconnect cannot abort it or its audit row.
  • On SIGTERM or SIGINT the server refuses new /v1/forget and /v1/erasures requests with 503 SERVER_SHUTTING_DOWN (Retry-After: 5) and waits up to CORTEX_SHUTDOWN_DESTRUCTIVE_WAIT_SECS (default 60; 0 skips the wait) for the ones in flight before the final snapshot persist.

Repairing derived data written by earlier builds

Enrichment-on stores written by v0.9.13 and earlier can hold facts with a lost negation, facts of different people merged, names in two scripts merged, several non-Latin names on one colliding entity id, and beliefs counting facts of the opposite polarity. 0.10.1 stops new damage; these trusted-operator routes repair what is already stored, from the event log, without wipes and without re-ingesting anything:

  • POST /v1/admin/derived/rederive (admin.compact; dry run by default). Takes a scope and an audit report from scripts/derived_corruption_audit.py, or event_ids, fact_ids or exact supersession edges; billing_tenant (default the caller's; captures written with minted tokens bill to their own tenant, and a job skips other tenants' events); actions (any of revert_supersessions, purge_derived, reenrich, reconsolidate, rebuild_beliefs; default all five); max_events (default 200, at most 2,000); job_id; cold_backup_ack; and enrichment_wait_secs (default 600, at most 3,600). The dry run returns the whole plan (each event's eligibility, every supersession edge considered, what the purge deletes, what it keeps, warnings, and a cost estimate priced from CORTEX_MODEL_PRICES) and changes nothing. An apply answers 202 with the job record and a Location header. A job resumes from its last completed phase when the same body is posted again with its job_id, and continue_from takes the events a max_events cut left (once per job: 409 JOB_ALREADY_CONTINUED with details.continued_by). A colliding legacy entity id (ent_______) reaches the facts of every subject it names; the plan lists them in ambiguous_entity_ids with a warning.
  • GET /v1/admin/derived/rederive/{job_id} (diagnostics.read) reads a job; a job another caller created is a 404.
  • POST /v1/admin/derived/resolver/unmerge (admin.compact; dry run by default) splits names the entity resolver merged at edit distance 1-2 (Priya into Riya).

An apply reverts the flagged supersessions, purges the derived rows the targeted events alone support, re-extracts those events with 0.10.1's extraction, reconsolidates the affected entity ledgers and rebuilds only the affected belief topics. Episodes, caller-declared beliefs and untouched topics are kept. Concepts it purges are re-synthesized by the runbook's step 6 with POST /v1/understanding/synthesize. Every request writes a DERIVED_REDERIVE:<decision> audit row. A job refuses to start next to a running forget or erasure, and a forget or erasure under a running job's scope root answers 409 REDERIVE_IN_PROGRESS (Retry-After: 30).

scripts/derived_corruption_audit.py (tool version 2) is read-only and uses GET routes only. It reports negation_lost, negation_introduced_by_supersession (informational), cross_name_merges, lossy_entity_ids and colliding_entity_ids, belief_polarity_mismatch, and since tool version 2 negation_doubled and negation_dropped; it compares subject ids ignoring letter case, separators and a leading article.

Follow docs/DERIVED_REDERIVE_RUNBOOK.md step by step: audit (on v0.9.13, before the upgrade), quiesce ingestion, cold backup, dry runs, apply, re-audit, re-synthesize concepts, resume. It also says what the audit cannot see (a denial an older extractor stored as an affirmative, which superseded another value) and how to check for it by hand, and which re-audit findings are expected after a repair (the "Known issues" below, and doubled negations the verifier-gated rules still write when the verifier gives no answer: re-run the job on those once it is healthy).

Rollback floor. Upgrading and dry runs leave the store openable by v0.9.13. An apply does not: v0.9.13 cannot read the repair's log rows and would bring the reverted supersessions back the first time it rebuilds its fact store. After an apply, going back to v0.9.13 means restoring the cold backup taken before it (see "Rollback").

Code plane, tokens, console and usage

  • Code plane (CORTEX_CODE_PLANE, off by default; an English-only plane, so the startup warns ENGLISH_ONLY_PLANE_ENABLED when it is on):
    • A repository whose scope no roster governs is never served to a per-actor token (404, left out of listings). Registering one provisions an unregistered team scope with the caller as Owner; a root is never claimed (403 SCOPE_REGISTRATION_REQUIRED). Recall, answer and compose code evidence re-check every repository against the current caller.
    • Credential files are never read; plain directories index only files of a registered grammar; /v1/code/context serves only indexed, hash-verified files. Scans are bounded (entries, depth, bytes, wall clock) and never walk pseudo-filesystems.
    • CORTEX_CODE_REPO_ROOTS limits where repositories may be registered (the request path is canonicalized and must lie inside a root; a value with no usable root refuses every registration). The filesystem root, /proc, /sys, /dev, /run, /boot, /etc and the data dir are always refused (422 REPO_PATH_NOT_ALLOWED, details.reason).
    • New bindings are named after the repository (<parent>/repo:<name>-<12 hex>); a binding another repository already holds is refused (409 REPO_SCOPE_IN_USE).
    • Call graphs are more accurate for C and C++; SCIP imports derive compiler-tier call edges; certify takes a paths filter (400 INVALID_PATHS for a bad one); context returns the snapshot generation. docs/CODE_PLANE.md ships in the image.
  • Tokens. POST /v1/auth/tokens accepts capabilities as an alias of caps and validates scopes entries.
  • Admin console. Confirm dialogs act on OK; events_stored counts each memory once (the figure halves on unified-log stores); settings show unwired lanes as not configured; the update check reads GitHub Releases, is cached, and can be turned off (CORTEX_UPDATE_FEED_URL=off, or pointed at your own mirror); the changelog lists every bundled release, newest first; derivation status reports off when it is off; the boot config dump marks values that came from an admin-UI override; not-configured 503s no longer count in errors_total (they have their own not_configured_total); a console restart exits with code 75 (EX_TEMPFAIL), so run the server under a supervisor that restarts on a non-zero exit.
  • Usage ledger. Streamed answers record their input tokens (they were booked at input 0), and the streamed diagnostics frame omits token counts it does not know instead of sending null.
  • Sample LiteLLM gateway. deploy/litellm/config.yaml no longer caches embeddings: LiteLLM 1.93.0 answers a partly cached embedding batch with duplicate indices, which the server rejects, so one cache hit failed a whole /v1/experience/bulk batch. Remove embedding and aembedding from supported_call_types in any copy of that config you run.
  • Tools. scripts/cold_backup.py restore creates the target's parent directory.

Operator and admin routes

New routes (all documented in docs/schemas/openapi.json):

RouteAccessPurpose
POST /v1/admin/derived/rederivetrusted operator, admin.compactPlan (dry run, default) or run a re-derivation job
GET /v1/admin/derived/rederive/{job_id}trusted operator, diagnostics.readRead a re-derivation job
POST /v1/admin/derived/resolver/unmergetrusted operator, admin.compactReport (dry run, default) or split names the resolver merged
GET /v1/admin/events/{id}/chunkstrusted operator, diagnostics.readHow one event was chunked, with fragment offsets
POST /v1/admin/index-audit/source-blob-linkstrusted operator, diagnostics.read (dry run) or admin.compact (apply)Backfill source_blob_ids on events written before 0.10.1
GET /v1/admin/index-audit/orphan-blobstrusted operator, diagnostics.readLive blobs no surviving event links or references

Changed routes:

  • GET /v1/admin/version: capabilities[].
  • GET /v1/admin/metrics: erasure_index (wal.ready, wal.backfill_complete, wal.catchup_pending_offsets, wal.rows_indexed, retry_candidates_ready, retry_candidates, and the discovery_full_scans / survival_full_scans still taken), erasure_residual (pending keys, parked segments, the journal, the last pass) and enrichment_multilingual (including the fact_surface counters).
  • GET /v1/admin/layers/stats: belief_reconcile (seeded_topics, dirty_scopes, dirty_topics, tick_in_progress, max_topics_per_scope_per_tick, done).
  • /metrics: the three multilingual enrichment counter families.
  • GET /v1/erasures/{id} (now documented): log_residual_cleared_at, log_residual_pending, summary.purge.blobs, summary.purge.retained_shared_blobs.
  • GET /v1/conflicts (now documented): confidence.

Environment variables

Every variable in the table is new since v0.9.13. docs/generated/env-vars.md has the full rows, and PRODUCTION_CONFIG.md the operator-facing ones. Leave the defaults unless a row says otherwise.

VariableDefaultWhat it does
CORTEX_RECALL_BUDGET_EXCERPTSonBudget excerpts and the strict budget (0, off or false restores the pre-0.10.1 render)
CORTEX_EVIDENCE_ROLE_CLASSIFIERpackerAnswer-context evidence classifier (full, legacy)
CORTEX_CONTEXT_STRIP_ECHOpairedEcho strip (all, 0)
CORTEX_CONTEXT_RENDER_SKIP_OVERSIZEDonSkip an oversized event and keep packing
CORTEX_FACET_PACK_POLICYtieredFacet merge policy (legacy; byte_share, tiered_byte_share, tiered_facts_late for evaluation)
CORTEX_CONTEXT_ASSISTANT_EXCERPTSoffOpt-in assistant-turn excerpts (1, ms)
CORTEX_CONTEXT_ASSISTANT_EXCERPT_MAX_BYTES600Per-event cap of those excerpts
CORTEX_QUERY_VARIANTS_FUSIONinterleaveVariant fusion (evaluation: depth_cap, deep_rrf)
CORTEX_QUERY_VARIANTS_DEPTH10Depth for depth_cap (evaluation)
CORTEX_EMBEDDING_CACHE_DISABLEunsetMeasurement only: bypass the provider embedding cache
CORTEX_CHUNK_FRAGMENTSonWrite chunk fragment rows
CORTEX_ENRICHMENT_SKIP_NON_ENoffSkip LLM enrichment for non-Latin-script text
CORTEX_POLARITY_RECHECKonThe polarity re-check call (0 records the doubt without a call)
CORTEX_POLARITY_QUOTE_TRIGGER_ENonThe English quote trigger of that re-check
CORTEX_ENRICHMENT_SCOPE_QUIET_MS0 (off)Hold a busy scope's captures for one batched extraction (at most 60000)
CORTEX_ENRICHMENT_SCOPE_QUIET_MAX_MS30000Longest hold of that window
CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS120How long a capture waits for a forget's or erasure's lease (1 to 3600)
CORTEX_SHUTDOWN_DESTRUCTIVE_WAIT_SECS60How long a graceful stop waits for running forgets and erasures (0 to 3600)
CORTEX_ERASURE_RESIDUALonThe raw residual pass (0 off; a startup warning says so)
CORTEX_ERASURE_RESIDUAL_SHUTDOWN_SECS20Residual drain at a graceful stop
CORTEX_ERASURE_RESIDUAL_MAX_BYTES536870912SST bytes one pass may rewrite per store
CORTEX_ERASURE_RESIDUAL_FULLTEXT_MAX_DOCS1000000Largest full-text segment one pass merges
CORTEX_ERASURE_RESIDUAL_MAX_PENDING_KEYS131072Recorded keys waiting per store
CORTEX_ERASURE_RESIDUAL_MIN_INTERVAL_MS30000Minimum gap between passes
CORTEX_ERASURE_RESIDUAL_DEBOUNCE_MS1000Quiet time before a pass starts
CORTEX_ERASURE_RESIDUAL_PARKED_RECHECK_SECS900Recheck interval of a parked over-budget segment
CORTEX_ERASURE_PERIODIC_COMPACTION_SECSunsetPeriodic compaction backstop for the stores (for example 86400)
CORTEX_ERASURE_INDEXonRead the erasure index (0: maintained, never read)
CORTEX_ERASURE_INDEX_BACKFILLonBackfill the index on a store written before it
CORTEX_ERASURE_INDEX_BACKFILL_ROWS20000Rows per backfill step
CORTEX_ERASURE_INDEX_BACKFILL_PAUSE_MS20Pause between backfill steps
CORTEX_CODE_REPO_ROOTSunset (startup warning)Directories code-plane repositories may be registered from
CORTEX_EVAL_FACET_PLAN_OVERRIDEoffEvaluation only; never in production
CORTEX_EVAL_DUMP_ANSWER_POOLSoffEvaluation only (writes customer content to disk); never in production

Changed: CORTEX_UPDATE_FEED_URL now defaults to the GitHub Releases API of the repository releases are published to, and accepts off (also 0, false, none, disabled) to turn the admin console's update check off (TELEMETRY.md, "Update check"); the check is cached for six hours (fifteen minutes after a failure) and sends no install id, key or usage data. CORTEX_ABSTAIN_SELF_RESCUE gains the opt-in value guarded; unset or 1 is the previous pass and 0 still turns it off. CORTEX_QUERY_EMBED_CACHE_CAP (default 512) is unchanged and now documented.

Error codes

New codes, and existing codes with new triggers:

StatusCodeWhereRetriable
422INVALID_BODYA request field whose capability this server does not list (details.unsupported_fields, details.required_capabilities); a malformed recall language, scripts, query_variants, temporal.timezone or temporal.refers_during (details.field)no
422INVALID_ENVELOPEA malformed capture language, scripts, context.timezone or context.refers_tono
422BLOB_TEXT_ENCODING_UNKNOWNA capture referencing a text blob whose bytes are not valid in any declared or detectable encodingno
409DESTRUCTIVE_OPERATION_IN_PROGRESS/v1/experience, /v1/experience/bulk: a new scope inside a running forget's or erasure's footprint, after CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS; a re-derivation job next to oneyes (Retry-After: 5)
409SCOPE_STATE_CHANGED/v1/scopes register, member edit and delete inside that footprint; a capture whose lease wait ended just as the lease was releasedyes
503SERVER_SHUTTING_DOWN/v1/forget, /v1/erasures after a graceful stop began; nothing was changedyes (Retry-After: 5, against the restarted server)
409REDERIVE_IN_PROGRESSA forget, an erasure, their previews or a second re-derivation on a scope root a re-derivation job holdsyes (Retry-After: 30)
409ENRICHMENT_OFFPOST /v1/admin/derived/rederive with enrichment offno
409JOB_MISMATCHA re-derivation job_id reused for other targets, actions or billing tenantno
409JOB_NOT_COMPLETEDcontinue_from names a job that has not completedno
409JOB_ALREADY_CONTINUEDcontinue_from names a job another job already continued (details.continued_by)no
404JOB_NOT_FOUNDcontinue_from names an unknown or foreign jobno
422COLD_BACKUP_ACK_REQUIREDA re-derivation apply with purge_derived and no cold_backup_ack: trueno
422NO_ELIGIBLE_EVENTSA re-derivation apply that would purge or re-enrich, with no eligible event; nothing changedno
422NO_TARGETSA re-derivation or unmerge request that names nothing to repair, or a continue_from job that left nothingno
422INVALID_AUDITA re-derivation or unmerge audit report that cannot be readno
503REDERIVE_UNSUPPORTEDThe coordinator cannot run re-derivationno
500REDERIVE_FAILED, REDERIVE_PLAN_FAILED, UNMERGE_FAILEDPlanning or preparing a re-derivation or an unmerge failed (the message says why)after the cause is fixed; a job with a record resumes with its job_id
409UNIFIED_WAL_REQUIREDsource-blob-links on the legacy dual WALno
500SOURCE_BLOB_LINKS_FAILED, ORPHAN_BLOB_REPORT_FAILED, CHUNK_INSPECTION_FAILEDFailures of the new operator routesyes
422EVAL_OVERRIDE_DISABLED/v1/answer with facet_plan_override on a server without the evaluation flagno
403SCOPE_REGISTRATION_REQUIREDCode plane: registering a repository directly under a root scopeno
409REPO_SCOPE_IN_USECode plane: the binding another repository already holdsno
422REPO_PATH_NOT_ALLOWEDCode plane: a path outside CORTEX_CODE_REPO_ROOTS or always refused (details.reason)no
400INVALID_PATHSCode plane: a malformed certify paths filterno

Codes no longer returned in these cases: 403 POLICY_DENIED for a capture that waited on a running forget (now 409 DESTRUCTIVE_OPERATION_IN_PROGRESS); 502 FORGET_BACKEND_FAILED for a forget of another scope's event id (now 200, every count 0); 502 INDEXING_FAILED for a wait=indexed capture a concurrent forget blanked.

Startup warnings

Advisory log lines, never a refusal; the defaults log none of them:

  • ENGLISH_ONLY_PLANE_ENABLED: CORTEX_ARTIFACT_DUAL_WRITE, CORTEX_ARTIFACT_ROUTES, CORTEX_SUMMARY_ROUTES or CORTEX_CODE_PLANE is on; those planes use English-only cues.
  • DOCUMENT_PROVIDER_FORGET_RESIDUE: a server document processor is configured; a forget by id leaves its whole-document derived text behind.
  • DOCUMENT_PROVIDER_NATIVE_DEAD_LETTERS: CORTEX_DOCUMENT_PROVIDER=native yields no usable text for PDF, DOCX and the like.
  • ENTITY_GRAPH_ENRICHMENT_FALSE_MERGE: the entity graph is on with enrichment; the graph resolver can merge near-identical names.
  • ENRICHMENT_RESOLVER_ERASURE_RESIDUE: enrichment on with the graph off; an erased capture's entity names stay in the resolver's files.
  • ERASURE_RESIDUAL_DISABLED, ERASURE_RESIDUAL_LEGACY_WAL: the raw residual pass is off, or cannot cover the legacy dual WAL.

Rollback

A v0.9.13 binary opens and serves a data dir 0.10.1 has used: no column family was added (the erasure index, the fact fence, the re-derivation job records and the fragment rows live in existing families, under keys earlier binaries never read), and 0.10.1's context changes store nothing. Stop 0.10.1 with a 120 s grace, start the kept v0.9.13 artifact on the same data dir, and set the 0.10.1 container's restart policy to no first. Three things end or limit a rollback:

  • After a derived-data repair apply, roll back only by restoring the cold backup taken before it. Restore into a new directory and start v0.9.13 on that one; keep the upgraded directory, and make sure nothing starts either build on it again. Captures and forgets served after the backup must be sent again.
  • During a rollback window v0.9.13 erases the way it always did: it deletes no blobs, leaves the raw residual, ignores the erased-fact fence (a crash can bring back a fact 0.10.1 erased), records no fence for its own erasures, builds beliefs without claim keys, and reads the new event fields (language, scripts, context.timezone, context.refers_to, source_blob_ids) as absent; a row it rewrites in place (a redaction) may lose them. Clients gating on capabilities[] stop sending the new fields on their own.
  • Entity-id lookups on v0.9.13 come back only after a belief rebuild. 0.10.1 rebuilds beliefs under its own entity ids, so after rolling back, GET /v1/beliefs?about=ent_... with a v0.9.13 id (such as ent_Jos_) returns nothing on v0.9.13 until you run POST /v1/beliefs/build for each scope there. That rebuild uses v0.9.13's lossy ids again. Do not rely on ent_ ids cached across a rollback.

After upgrading again

  1. Delete the orphan blobs of events erased during the window (GET /v1/admin/index-audit/orphan-blobs, then DELETE /v1/blobs/{id}; check each first, because the report also lists blobs of live events written before 0.10.1), and set CORTEX_ERASURE_PERIODIC_COMPACTION_SECS for a while to reach the raw residual.
  2. Facts: a fact 0.10.1 erased before the rollback is dropped again at the start. A fact that v0.9.13 erased or forgot during the window and that a v0.9.13 crash brought back is not: it is still served. Keep, for every forget and erasure you send during a rollback window, its body, the time you sent it and its response; the v1 audit log (CORTEX_V1_AUDIT_PATH) records each request's endpoint, actor, time, status and a peppered body hash, not the body.
    • Re-issue as it was only a request that names memory ids (selector.memory_ids of evt_ or fact_ ids), without its from_preview_id and idempotency_key: it names exactly what it named before. An erasure re-issued this way matches facts by the events they came from even though the events are gone, and reports them in summary.deleted_facts.
    • Never re-issue any other request as it was (a scope-wide forget, an about_subject, about_entity or predicate forget, an ent_ id, a whole-scope erasure): none has a time bound, so re-issued later it also deletes what was written after the original, and a scope-wide redaction or erasure also moves the scope's forget tombstone. Remove instead exactly what came back: the facts in the request's scope that its selector matches, that were created before it was sent and that no later capture supports, forgotten by fact_ id. Take the time of the request from the server (the response's audit_id or erasure_id is a UUIDv7 that carries it), and leave facts recorded within a couple of minutes of it to a person's review.
    • Never turn a forget into an erasure: a forget that named only layers: ["facts"] left its event live on purpose.
  3. Beliefs: on incremental-layer deployments every start sweeps the keyless beliefs a v0.9.13 binary wrote; elsewhere rebuild them with POST /v1/beliefs/build for each scope.
  4. The erasure index catches up the rows v0.9.13 appended during the window, in the background, before erasures read it again.

docs/ERASURE_COVERAGE.md ("Rollback window") has the details.

Known issues

Enrichment-on deployments only (with enrichment off there are no facts or beliefs):

  • A belief can list a support of another value and the opposite polarity. For a predicate the built-in ontology does not know (habit, habit_frequency, no_longer_works beside works_in), the belief builder groups facts of different values into one belief, so "prefers email updates" can list "does not want SMS updates any more" among its supports. Neither fact is wrong; the grouping is. The audit reports these as belief_polarity_mismatch.
  • A cessation can retire a fact that is still true. A background consolidation stale verdict has no same-claim check across a cessation, so "prefers email updates" can be retired by "does not want SMS updates any more" (the audit's informational negation_introduced_by_supersession).
  • Sentence-only updates of a subject that is not a name no longer supersede ("User lives in Boston", then "User lives in Denver": both stay live, where v0.9.13 retired the older one). An update extracted as a triple still supersedes.
  • Doubled negations can still be written when the verifier gives no answer. A negated claim whose object opens with "no", "without", "none" or "nothing" keeps the extractor's polarity when the re-check is off (CORTEX_POLARITY_RECHECK=0), no verifier is configured, the call fails, or the event's call budget is spent. The audit lists them as negation_doubled; re-run a re-derivation on them once the verifier is healthy.
  • Entity-extraction spend is not priced. GET /v1/admin/usage records the entity-extraction lane with tokens but no model, so it is always under unpriced_calls and CORTEX_MODEL_PRICES cannot price it. Read the provider's bill for the total.
  • Erased names stay in the enrichment resolver with the entity graph off (the default); the startup warning ENRICHMENT_RESOLVER_ERASURE_RESIDUE says so.
  • An English denial the extraction model writes as an affirmative is re-checked only for a triple on a relation the built-in ontology knows whose quote puts the negation before the object; otherwise it is stored affirmative, and the audit cannot see it either (docs/DERIVED_REDERIVE_RUNBOOK.md, "What the audit cannot see").

Everyone:

  • An excerpt can take the room of the last whole event in a full /v1/answer context. When the recall passes bring more than the request-wide budget holds, an excerpt placed at its event's rank can move the lowest-ranked whole event, already cut short, out of the context. CORTEX_RECALL_BUDGET_EXCERPTS=0 restores the previous contexts.
  • A client that merges several /v1/recall blocks itself can recognize a budget excerpt from its layers.events item (_partial_reason: "budget_excerpt") only while the event is still in the layer; the server's own /v1/answer merge knows every excerpt it placed.
  • Cross-script recall is below the quality bar. A Latin-script query over memories in Devanagari or another script often misses; send a query variant in the memories' script.
  • directives.extract is honored on POST /v1/experience/bulk only. On a single-event POST /v1/experience it is lost on the default unified WAL and enrichment runs as if none were set; send a one-item bulk request until that is fixed. directives.embed is accepted and validated but not enforced.
  • Blob backfill is per billing tenant. source-blob-links and orphan-blobs are confined to the caller's billing tenant, and the static operator key is default: the pre-0.10.1 blobs of captures written with minted tokens of another tenant cannot be linked or reviewed by the operator in this release.
  • Scope-wide purge.events counts derived content rows (a fact, gloss or question-key row stored under the scope) as events; trust deleted_events and redacted_events for a scope.
  • fulltext_segment_over_budget may never clear on a store whose full-text segments exceed the pass's budget; it never clears falsely.
  • A server document processor leaves residue on forget by id: its whole-document derived text stays recallable (DOCUMENT_PROVIDER_FORGET_RESIDUE).
  • matched_fragments lists only chunks a channel returned, so a document found through one chunk cites that chunk, not necessarily the one holding the answer.
  • POST /v1/experience?wait=settled is still accepted but cannot succeed and returns 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.

Clients

These client versions are released with 0.10.1. Every client passes the new response fields through unchanged, and each new request parameter is sent only when you give it, so an older client keeps working against 0.10.1.

PackageRegistryPrevious versionReleased with 0.10.1What is new
cortexdbai (Python)PyPI0.11.20.12.0version(), capabilities(); capture language, scripts, timezone, refers_to; recall language, scripts, query_variants and the temporal keys; delete_blob(); event_chunks(); recall diagnostics types; fragment_text / iter_matched_fragments (an excerpted item yields no span, like any partial view)
cortexdbai (TypeScript)npm0.9.30.10.0The same surfaces: version(), capabilities(), the capture and recall hints, deleteBlob(), eventChunks(), fragmentText / matchedFragments; the excerpt and receipt fields pass through and are documented on the types
cortexdb-mcpPyPI0.7.50.8.0Matched document spans in memory_search output; clearer code-plane tool output
cortexdb-cliPyPI0.5.40.6.0cortexdb auth tokens --scope is now where your auth.mint is checked, and --confine CAP:PREFIX carries the token's scopes
cortexdb-connectorsPyPI0.2.210.2.22GitHub: full PR, issue, comment and push bodies, REST-list backfill (GITHUB_BACKFILL_DAYS), object-identity record keys; read connectors/CHANGELOG.md before upgrading, because GitHub records are re-keyed

Notes

Single-node remains the only supported topology.

Erasure and forget were measured up to 5,000,000 events; 25,000,000 was not measured.

The online re-embed job remains designed and unbuilt; v0.9.12's flagged drop-and-rebuild is the offline path.

v0.9 series
14 releasesJuly 28, 2026 – September 18, 2026
v0.9.13

v0.9.13

The durable token and cost accounting v0.9.12 added is now visible in the admin UI, on the Observability page where you were already looking.

Full notesHide notes24 notes

BEFORE YOU UPGRADE

Nothing here changes a contract. No new upgrade notes since v0.9.12.

This release also carries a dependency security fix (rustls, RUSTSEC-2026-0285) that affects outbound provider calls. See SECURITY below.

WHAT YOU GET

Open the admin UI and go to Observability. There is now a Tokens & cost block reading the durable ledger, and a month-to-date spend strip on Overview.

Month to date and Since inception are one click each. Cost and tokens by role, with calls and mean latency — slow-and-cheap is a different problem from fast-and-expensive. A per-day trend, and a by-model table carrying exact tokens beside the model name with an implied rate per million, so you can re-price at your own rate. Enrichment coverage: events enriched, facts per event, failed, and the backlog, which is labelled a live gauge because that is what it is. Per-scope totals, click to filter. A CSV export of the same buckets, flat, for a billing pipeline.

WHAT THE PAGE WILL NOT DO

It will not show you a number that looks measured when it is not. That is the point of the page, not a detail of it, and the same release hardens it twice.

Three marks, distinguishable without hovering:

exact a plain number. a floor "≥ n" in amber, when the provider reported only part of the usage. The entity-extraction lane does this: it returns one combined token count with no input/output split, so those tokens are held out of the split rather than priced at an output rate, which would overstate that lane several-fold. unmeasured a hatched chip carrying a WORD, never a figure. A model this build has no rate for is "unpriced", not "$0.00" — because zero dollars there means we could not price it, not that it was free. Set CORTEX_MODEL_PRICES to give that model a rate.

Two roles do not fully reach the ledger and the page says so rather than reading as zero. The reranker records nothing at all: no token or cost data crosses its interface, and its provider bills per search unit rather than per token. The answer lane records every call site except four small fact-join calls.

Those two are NOT the same thing, and this release fixes a bug where the page treated them alike. A window with no answer calls was drawn as "no tokens, no dollars, and none will be recorded", which is false and false in the expensive direction, about the lane most likely to dominate a bill. /v1/admin/usage now publishes coverage as full, partial or none, so a reader can tell "this lane was idle" from "this lane is invisible". A partly-counted lane with no calls now says exactly that, and that a future window can under-report it.

Also fixed: the card header asserted durability before the data arrived and was never corrected, so an in-memory ledger showed "these counters survive a restart" directly above a red banner saying they do not.

The instrumentation and cost_basis blocks are now documented in docs/schemas/openapi.json. Every honesty mark is derived from them, and they were previously undocumented.

SECURITY

rustls is updated 0.23.37 -> 0.23.45, which carries the fix for RUSTSEC-2026-0285: TLS 1.3 handshake messages were accepted across encryption level boundaries (CVSS 5.3). CortexDB serves plain HTTP and terminates no TLS itself, so the exposure is on outbound calls to embedding, reranker and LLM providers, not on the listener. Taking the fix pulls aws-lc-rs to 1.18.1 and its compiled backend aws-lc-sys to 0.45.0; the full Linux suite was re-run on that lockfile rather than inherited from the previous one.

KNOWN GAP

This repository has no JavaScript test harness, so the page's rendering rules — including the unpriced-is-not-zero behaviour above — are not pinned by anything CI re-runs. The Rust tests cover the wiring only. Worth knowing if you edit it.

CLIENTS

Unchanged from v0.9.11: cortexdbai 0.11.2 on PyPI and 0.9.3 on npm, cortexdb-connectors 0.2.21, cortexdb-mcp 0.7.5, cortexdb-cli 0.5.4.

NOTES

Single-node remains the only supported topology.

POST /v1/experience?wait=settled is still accepted but cannot succeed and returns 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.

The online re-embed job remains designed and unbuilt; v0.9.12's flagged drop-and-rebuild is the offline path.

v0.9.12

v0.9.12

Two things a pilot asked for by name: you can see what the pipeline costs, and you can change embedding model on a corpus you already have.

Full notesHide notes24 notes

BEFORE YOU UPGRADE

Nothing here changes a contract. If you are on v0.9.11 there are no new upgrade notes. If you are further back, read the v0.9.10 notes first — scope types are validated when a scope is created, and an explicitly configured embedding endpoint with no credential is a boot refusal rather than a silent fallback to meaningless vectors.

DURABLE TOKEN AND COST ACCOUNTING

GET /v1/admin/usage reports tokens and dollars per pipeline role — embeddings, entity extraction, enrichment, recall, answer, verifier and the background maintenance lanes — and the numbers SURVIVE A RESTART. One bucket per UTC day, role, tenant and model, so summing a range answers month-to-date or since-pilot -start. ?format=csv exports the same buckets flat.

The counter that existed before this release reset to zero on every restart, because it had only ever been process memory. There was no record to lose. Historical spend from before you upgrade cannot be reconstructed, and no estimate here will pretend otherwise.

Read by_role_model, not just the totals. It carries exact token counts beside the model name, which is what lets you recompute dollars at a corrected rate, or at your own rate if you bring your own model, without re-running anything. Cost is a view over the durable record; the tokens are the durable record.

Three fields exist so the report cannot flatter itself:

unpriced_calls this build has no rate for that model. The tokens are exact, the dollars are zero, and zero here means unmeasured rather than free. Set CORTEX_MODEL_PRICES to give it a rate. tokens_unsplit a provider that reported one combined number instead of an input/output split. Kept separate on purpose: pricing it at an output rate overstates that lane several-fold. incomplete_calls while non-zero, the token fields are a floor.

Two roles are declared uninstrumented rather than quietly read as zero: four calls inside the answer path discard their usage before it can be recorded, and the reranker's interface carries no token or cost data at all (its provider also bills per search unit rather than per token). Both say so in the payload.

Pricing itself was wrong before this release and is worth re-reading if you quoted it. The enrichment lane billed every model at the cheapest model's rate — roughly seventeen times understated if you run gpt-4o there — and anything whose name contained "opus" was billed at the 4.0 rate, roughly three times overstated for claude-opus-4-6. An unrecognised model silently took sonnet's price. One price book now serves every lane, and an unknown model is reported rather than guessed.

IN-PLACE EMBEDDING MIGRATION

Changing embedding dimensions on an existing data directory used to be a hard boot refusal with no way forward except exporting and re-importing into a fresh store. CORTEX_EMBEDDING_REBUILD_VECTORS=1 now authorizes exactly one drop-and-rebuild: the vector indexes whose width no longer matches are dropped and recreated empty at the new width, and the store reports itself incomplete until refilled.

Read docs/EMBEDDING_MIGRATION_RUNBOOK.md before using it. The honest cost: vector recall returns nothing between the drop and the refill, BM25 only, for as long as your corpus takes.

The safety properties, stated because this destroys data on purpose:

It is never automatic. An unattended dimension mismatch — a config typo, a model renamed upstream — still refuses to boot, exactly as before. It takes two deliberate flags. The repin flag and the rebuild flag are separate, and the destructive one cannot act without the other. It is spent after one use. A flag left in a unit file authorizes nothing on the next boot. It will not drop a real corpus for mock embeddings. That is what a lost embedding credential looks like, and it is refused with the data untouched. A refused boot now leaves the data directory byte-identical. Previously a failed attempt rewrote the pin first, so the store could no longer boot on its own correct configuration without a second override.

The index audit reports all four vector families on every call, not just chunks. It can no longer answer "clean" over a store whose entity or derived vector spaces are empty. It also states plainly that its repair rebuilds EVENT chunks only: enrichment writes fact, triple and summary vectors into the same index and the audit does not re-derive those, so a clean chunk audit means "every event has its chunk vector", not "this index is whole".

ALSO

The reranker can point at an endpoint you control (CORTEX_RERANKER_URL).

CLIENTS

Unchanged from v0.9.11: cortexdbai 0.11.2 on PyPI and 0.9.3 on npm, cortexdb-connectors 0.2.21, cortexdb-mcp 0.7.5, cortexdb-cli 0.5.4.

NOTES

Single-node remains the only supported topology.

POST /v1/experience?wait=settled is still accepted but cannot succeed and returns 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.

The online re-embed job — a route pair with a dry-run cost estimate, dual-embed of writes landing during the job, and an atomic swap with no offline window — is designed and not built. This release is the offline path.

v0.9.11

v0.9.11

A data-loss fix, and the tests that should have caught the v0.9.9 authentication bypass before a reader reported it.

Full notesHide notes21 notes

BEFORE YOU UPGRADE

Nothing in this release changes a contract. If you are on v0.9.10, the upgrade notes there still apply and no new ones are added.

If you are still on v0.9.9 or earlier, read the v0.9.10 notes first: scope types are validated when a scope is created, and an explicitly configured embedding endpoint with no credential is now a boot refusal rather than a silent fallback to meaningless vectors.

FIXED

An embedding-provider outage no longer discards acknowledged writes.

This is the reason for the release. A transient failure of the embedding endpoint during indexing did not merely leave the write pending: the indexer durably excluded the event from recall within roughly half a second, and never reconsidered it after the provider recovered. The write had been acknowledged. It was gone, permanently, and POST /v1/admin/index-audit reported the store healthy because its check is a presence test that the excluded event does not appear in.

The recovery path now distinguishes four outcomes instead of a boolean, because the decision turns on whether the event reached the write-ahead log, and collapsing "never became durable" into "durable but its re-index failed" is the conflation the bug was made of. Vector repair under the strict policy now embeds before it writes anything, so an unavailable provider writes nothing rather than a partial row.

The trade this makes, stated plainly: a sustained provider outage now holds the indexer rather than dropping individual events, so lag grows and ?wait=indexed times out for the duration. Durability over liveness, which is the right way round for a write you have already acknowledged. It clears on its own when the provider returns.

GET /v1/experience/status no longer reports "indexing in progress" forever for an event the indexer has permanently given up on. It read only an in-memory map that does not carry that verdict and does not survive a restart; it now reads the stored record. An erased event carries the same marker and is still not reported as an indexing failure.

The keyless authentication posture is now honest in the boot log. The server logged that every request is served as a fixed local identity; in fact a keyless server serves each request as whatever actor the caller names, with that actor's capabilities, and the deployment preset does not restrict it. The log now says so, and the posture line reports whether unsigned actors are accepted.

To be explicit, since the v0.9.9 report that prompted this is public: the authentication bypass it describes was fixed in v0.9.10, not here. A request carrying only an actor header and no bearer has been refused since that release, both when you set an API key and when the server generates one for a network-exposed bind. What this release adds is the regression tests that pin it, including the generated-key path, and an end-to-end check of the exact configuration that was reported.

DOCUMENTATION

The getting-started guide now documents all three authentication postures, including the generated key written to the data directory on a keyless network-exposed boot, which was previously described only in source comments.

CLIENTS

cortexdb-mcp 0.7.5

Three tools could not succeed on any call in 0.7.4. memory_delete sent a selector together with a confirm-all flag, which the server refuses as ambiguous. advanced_search sent a temporal filter the server rejects as an unknown field, and silently ignored its own source and episode-type filters, which are now implemented rather than advertised and dropped. Cached authentication tokens were never refreshed, so once one expired every call returned 401 until the state file was deleted by hand.

The other clients are unchanged from v0.9.10: cortexdbai 0.11.2 on PyPI and 0.9.3 on npm, cortexdb-connectors 0.2.21, cortexdb-cli 0.5.4.

NOTES

Single-node remains the only supported topology.

POST /v1/experience?wait=settled is still accepted but cannot succeed and returns 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.

One caution for anyone migrating embedding providers by hand: the index audit's repair rebuilds chunk vectors only. Entity and derived vectors are written by the enrichment and derived-layer builds, and the audit neither rebuilds them nor reports them missing, so a store whose vector directories were deleted and repaired will report clean while those two spaces are empty. A supported migration path is designed but not yet built.

v0.9.10

v0.9.10

Retention and erasure become operator-driven, the native memory subsystem becomes something the binary actually serves, and a long list of correctness fixes lands underneath both.

Full notesHide notes28 notes

BEFORE YOU UPGRADE

Scope types are now validated when a scope is first created. A write that would create a new scope segment whose type is outside the deployment's defaults.allowed_scope_types is refused with 422 UNREGISTERED_SCOPE_TYPE. No earlier release enforced this on the write path, so it is new behaviour even though the setting is not.

Scopes that already exist are unaffected: the check runs only when a scope is being created, so existing traffic keeps working after an upgrade. What changes is the first write into a NEW scope carrying an unusual type. The shipped presets allow org, dept, team, app, user, agent, service, ws, project, global, system and source; dev_local additionally allows debug and temp. If your integration mints scopes with a type outside that set, add it to allowed_scope_types in your deployment policy before upgrading.

source is in that list because every CortexDB connector writes into a <root>/source:<connector> segment. Connectors are unaffected.

An explicitly configured embedding endpoint without a credential is now a boot refusal rather than a silent fallback to mock vectors. This affects the plain docker run cortexdb/cortexdb quickstart: the image sets an OpenAI embedding URL, so a container started with no embedding key now exits at startup where v0.9.9 would come up and serve. Supply OPENAI_API_KEY, CORTEX_EMBEDDING_API_KEY or LLM_API_KEY, or set CORTEX_EMBEDDING_PROVIDER=mock if you only mean to exercise control paths.

Mock vectors are not a degraded search, they are meaningless search, and the data directory is pinned to the provider that wrote it. A store filled with mock vectors cannot later be pointed at a real provider. Refusing to boot is the honest failure.

The startup extraction-cache purge is now opt-in. Earlier candidate builds deleted <data_dir>/extraction_cache on first boot, which is a pure cache but one holding paid LLM extraction responses. The default now reports what it would remove and its size, and only removes it when CORTEX_EXTRACTION_CACHE_PURGE is set.

NEW

Retention and erasure for the production WAL, driven by operator routes rather than a background policy you cannot see: /v1/admin/retention, /v1/admin/retention/policies, /v1/admin/retention/sweep, /v1/admin/wal and /v1/admin/wal/compact. Expiry is irreversible erasure, so dry_run defaults to true on every one of them and you must opt in to apply. cortexdb-cli 0.5.4 adds matching retention and wal command groups.

A commitments plane the binary serves, rather than one reachable only from an example host.

The native memory subsystem now has a real activation path: a host profile startup mode, production authority adapters, and an offline store migrator.

Universal enrichment and the artifact pipeline are integrated into the memory path.

FIXED, OPERATIONALLY VISIBLE

Erasure no longer commits the full-text index once per erased document. A 10,000-document erasure previously meant roughly ten minutes of fsync with ingest blocked behind the writer lock; it is now batched.

The server no longer refuses to start with "durable revocation store unavailable" after a fork, and a forked child can no longer lock the database out of its own store. Both were advisory locks released by descriptor rather than by owner.

Recall scoring no longer re-reads its policy from process-global environment per call, which could let two indexes score under different query plans within one query.

A code-plane purge now releases every full-text handle before reporting success, instead of reporting success while a live recall still held files.

Snapshot publication is atomic. Replacing a live name is not atomic for readers on Windows, and a published snapshot could read back as absent, which would have composed a certified answer over an empty scope.

Registry errors are classified consistently on every route that touches them instead of defaulting to 503, and a refusal body can no longer echo another tenant's billing identifier.

Synthesis admission capacity is released when the work finishes rather than a scheduler hop later, so a client that polls a job to completion and resubmits is no longer told the service is saturated.

SECURITY

h2 updated for RUSTSEC-2026-0258. The server lru cache dependency upgraded.

CLIENTS

cortexdbai (PyPI) 0.11.2 cortexdbai (npm) 0.9.3 cortexdb-connectors 0.2.21 cortexdb-mcp 0.7.4 cortexdb-cli 0.5.4

cortexdb-mcp 0.7.4 is an important upgrade. The published 0.7.3 declared an unbounded mcp>=1.0; upstream mcp 2.0.0 removed mcp.server.fastmcp, so every fresh install of 0.7.3 crashes with ModuleNotFoundError at import. 0.7.4 pins mcp>=1.0,<2.0. It also repairs the code_explore, code_inventory and code_graph tools, which raised TypeError on every call in 0.7.3, and stops tool output being joined with a literal backslash-n.

NOTES

Single-node remains the only supported topology. Cluster flags are refused at startup with CLUSTER_MODE_UNSUPPORTED and exit 78 rather than silently serving N independent databases.

POST /v1/experience?wait=settled is still accepted but cannot succeed and returns 503 SETTLEMENT_UNAVAILABLE. Use wait=indexed.

v0.9.9

CortexDB v0.9.9

One change, and it is about telling you the truth rather than doing something new.

Full notesHide notes7 notes

Readiness now reports whether the server can actually do its job. GET /v1/admin/ready gained three checks: embeddings, enrichment and content_processors.

Until now a server could return {"status":"healthy"} and degraded: false while quietly underdelivering in three different ways, and nothing on the wire distinguished that from a healthy deployment:

  • A configured embedding provider whose key had stopped working. Readiness could tell you the corpus was pinned to mock embeddings, but not that a real provider was returning 401 on every call. New writes got no vectors, every recall silently fell back to a degraded path, and /v1/answer returned nothing useful — for weeks, on a server reporting itself healthy. embeddings is now outcome-based: it records what real provider calls actually did.
  • Enrichment switched off. A deployment with facts, beliefs and concepts permanently empty looked identical to one still building them. Enrichment is off by default, which makes the silence worse.
  • No content processors configured. Uploads still return a blob_id, so "stored but nothing extracted or indexed from it" was indistinguishable from success.

Three states are distinguished deliberately. unproven — booted, nothing embedded yet — is not degraded: a server with no writes has not earned either verdict, and a check that cries wolf gets ignored. failing means the most recent real call failed, not "something failed once", so a transient rate limit does not pin a deployment to degraded and a rotated key clears it without a restart.

degraded is now true for mock or failing embeddings, and the check carries counts, timestamps and the provider's own error text, so an operator sees the 401 rather than inferring it.

If you run CortexDB with an external embedding provider, this is worth upgrading for: it is the difference between finding out from your monitoring and finding out from a user.

Drop-in. No API, schema or configuration changes.

v0.9.8

CortexDB v0.9.8

One fix, and it is the other half of the deletion work that shipped in v0.9.7: a scope-wide forget could leave the scope unable to accept new memories.

Full notesHide notes5 notes

A forget no longer blinds a scope to everything written after it. After POST /v1/forget with cascade: "redact_events" and confirm_all: true, memories written to that scope afterwards were accepted, acknowledged and durably stored — GET /v1/events/{id} served them — but /v1/recall never returned them again. Not a delay: they stayed invisible indefinitely, and every child scope created under that scope inherited the same blindness. The practical effect was that a scope you had once erased could never be reused, and nothing in the API told you so.

The erasure guarantee is unchanged. Memories written before the cutoff stay suppressed exactly as before; only writes that genuinely postdate the forget come back.

Who was affected. This only surfaced when retrieval fell back to its degraded path — most commonly when the configured embedding provider was failing (an expired or rate-limited API key), so ranked retrieval returned nothing on every query. A healthy deployment would not have seen it, which is why it took a production environment to reproduce. If your logs carry embedding missing for chunk or continuing with WAL-only events, you were in the affected mode, and it is worth checking your embedding credentials as well as upgrading: in that state new writes also get no vectors at all.

Nothing to migrate. Scopes that were already blinded recover on upgrade — the suppression was applied at read time, so the memories were never lost.

Drop-in. No API, schema or configuration changes.

v0.9.7

CortexDB v0.9.7

Two data-integrity fixes in the deletion path, and the JSON-attachment fix that unblocks connector uploads.

Full notesHide notes6 notes

forget now honours the cascade you send. The scope-wide pass ignored cascade and always ran a raw-event purge. So cascade: "derived_only" — which by definition keeps raw events — stripped your event documents out of the search indexes anyway, and reported deleted.events: N for events it had not removed from the store. It both destroyed more than you asked for and misreported what it did. A derived-only forget now leaves raw events intact and recallable, and no longer counts them as deleted.

Deleted records no longer stay fetchable by id. GET /v1/events/{id} checked only the per-event redaction mark, which only cascade: "redact_events" sets. When a forget suppressed a scope by tombstone instead, that mark stayed clear — so /v1/recall and /v1/events correctly hid the record while the by-id endpoint served it. /v1/facts/{id} had no such check at all. Both now return 404, matching the listing paths.

If you rely on /v1/forget for data-subject deletion, upgrade. The audit trail was accurate about the request but not about the outcome.

POST /v1/blobs accepts application/json. A guard meant to reject multipart wrappers rejected the whole application/json MIME, silently dropping every JSON attachment connectors tried to ingest — the one type the extraction pipeline supported end to end. Only unambiguous multipart is rejected now.

  • /v1/admin/ready, /v1/blobs, /v1/derivation/status and the /v1/auth/* routes are described in the served OpenAPI document, with schemas. 42 of 117 /v1 routes are now documented, up from 31, and CI fails on new drift or a dangling schema reference.
  • The wait=consolidated latency guidance was wrong: that stage blocks on an out-of-process LLM call, so its published ~500-3000ms band was not achievable. Documented as provider-bound; use wait=indexed when you need a bounded acknowledgement.

Drop-in. No API, schema or configuration changes.

v0.9.6

blob extraction repaired under the unified WAL

Patch release. Multimodal blob extraction works again on the unified-WAL write path — a regression present since v0.9.0, surfaced by an external real-data report. No storage format changes, no migration.

Full notesHide notes3 notes

The fix

  • Since v0.9.0 (CORTEX_UNIFIED_WAL default ON), single POST /v1/experience writes with content.kind=blob_ref were written and embedded but indexed as plain text: the indexer tail rebuilt events without the API-resolved physical BlobRef, so the configured Tika/vision/Whisper content processors were never invoked.
  • The tail now re-resolves the logical blob_id against the same blob metadata store the API resolves with (one shared instance), with the same owner ACL (DISC-024) and binary-only gate. Wired into per-item and bulk tail applies plus crash-reconcile, with regression pins.
  • Loud startup warning when content processors are configured without an LLM router (that combination gets no extraction scanner); the six processor env knobs are now registered with the boot config lint.
  • Blob events ingested while the regression was live were indexed as placeholders; re-ingesting them runs extraction (no auto-backfill).

Connectors (PyPI)

  • cortexdb-connectors 0.2.16: Slack content-versioned idempotency keys (edits/renames no longer 409-poison sync), live webhook edits ingest, distinct reaction keys end a 409/500 redelivery storm, empty signing secrets fail closed, stale-thread scan stops silent reply loss.
  • cortexdb-connectors 0.2.17: Unicode content gate, multi-channel cursor lookback, 5 MiB pre-auth webhook body cap, bad-UTF-8 400s, 3s Slack acks with background ingest, per-item dead-letter budgets, per-channel failure isolation, empty-window cursor hold, HTML unescape, correct private-channel visibility labeling.

Compatibility

  • /v1 surface unchanged; 0.9.x data dirs load as-is.
v0.9.5

the Code Intelligence Plane

The largest release since 0.9.0: CortexDB now has a code plane — a typed, versioned memory of your source code that recall and answers draw on. Plus the 10X performance campaign (boot in seconds, ~50% artifact-compile cost cut, a two-tier LLM response cache), answer-free context preview, and cortexdb-connectors 0.2.15 with a tl;dv bug-fix batch.

Full notesHide notes5 notes

The Code Intelligence Plane

  • Registered repositories are indexed into a content-addressed, generation-pinned code graph (definitions, imports, calls, resources) served with honest precision labels: every fact carries a precision tier (Syntax/Manifest/Compiler/History/Runtime) and a resolution status, and both travel into the answer. No global name-match fallback anywhere — "never guess" is a permanent regression test.
  • 43+ languages via sandboxed, budgeted tree-sitter extraction with a universal text fallback; never-guess per-language resolvers plus a SCIP compiler tier (compiler facts supersede, never rewrite).
  • New code_index RocksDB posting engine: delta segments, Roaring overlays, hard query budgets (unplanned deep traversal is refused, never silently truncated), as_of time-travel, freshness proofs, GC.
  • Opt-in semantic code search (CORTEX_CODE_EMBEDDING_*) with TQ2 HNSW, persistent-vector reranking, learned region selection; degrades to path/graph/BM25 on provider failure.
  • ~21 new /v1/code/* routes: symbol search/resolve, calls, impact, bounded graph query + export in nine formats, workspace context, history/for-symbol, tests/for-change, hash-verified certified answers (list-implementations, count-call-sites), SCIP + lcov import, export/import index bundles. Tenant isolation absolute.
  • Surfaces everywhere: code-memory feature bundle + Admin UI page, ten cortexdb code CLI verbs, client.code.* in Python/TS SDKs, four read-only MCP tools with --tool-profile code.
  • Benchmarked honestly under benchmarks/codeplane/ (blind, pinned competitors, append-only manifests): SWE-bench any@10 0.818 with p50 18 ms queries, 26/26 dynamic-multilang, ~22 bytes index/LOC, 25M LOC in 93 s.
  • Off by default and free when off: without CORTEX_CODE_PLANE nothing is constructed and /v1/code/* answers 503 CODE_PLANE_DISABLED.

10X performance campaign

  • Boot in seconds: binary snapshots (FSN3/BTP2/LYS3) + mutation journals, HNSW journal-replay warm boot, parallel hydration of all eleven stores. 26 s warm boot at 6.2M events.
  • Artifact lite profile (~-50% compile cost), deterministic no-LLM triage for signal-free turns, two-tier LLM response cache (46/48 calls replayed on a recompile), provider fleets with per-task routing and timeouts, fp16 side-CF embeddings (out of WAL/layer rows).

Answers & recall

  • POST /v1/answer accepts skip_answer_llm for answer-free context preview with route/certification diagnostics; certified deterministic reducers extended (temporal deltas, fail-closed date recovery).
  • Focused lexical views fused for noisy code tasks; direct/derived evidence interleaving; bounded derived-vector backfill.

Connectors 0.2.15

  • GitHub emits code_anchors (PR files, issue<->PR links, push shas) -> discussed_in/changed_by bridge facts against the code graph.
  • tl;dv real-data fixes (L1-L6): clean failure + held cursor on list 4xx; solo recordings labeled RESTRICTED; 24h cursor lookback ends silent drops of late-transcribed meetings; TranscriptReady stamps the meeting's happenedAt, not delivery time; real organizer names; locale-independent date parsing.

Compatibility

  • /v1 surface additive only; 0.9.x data dirs load with no migration (new snapshot formats write-forward with legacy read-compat).
  • CORTEX_ARTIFACT_WORKERS now gates artifact workers directly (unset = historical behavior; 0 = immutable read-only artifact mode).
v0.9.4

the new Admin UI

The embedded admin console has been rebuilt from the ground up. What was a single-page memories view is now a full operations app — still zero-dependency, still compiled into the binary (no CDN, works air-gapped), served at / on the API port.

Full notesHide notes4 notes
  • Thirteen pages: Overview, Memories, Console, Artifacts, Conflicts, Observability, Features, Settings, Logs, Data & scopes, Audit & access, Maintenance, and Updates.
  • A real design system: live bento Overview with throughput charts, interactive SVG charts with hover crosshairs and tooltips, count-up KPI tiles, a command palette (Ctrl+K), a notification center, and full mobile support.
  • Features page — plain-English toggles for the major capability bundles (Enrichment, Artifacts, Knowledge graph, Deep re-ranking, Time-travel facts, SQ8 vectors, Flat memory mode, Fast bulk upload, Background maintenance, Telemetry). Toggles persist across restarts, apply live where possible, and honestly report "restart required" where they don't.
  • Settings page — configure the ten AI/processor connections (embeddings, enrichment, recall ranking, answers, verifier, reranker, document/audio/video/image processors) from the browser. API keys are write-only: the UI only ever sees a configured flag and a last-4 hint.
  • Logs page — live tail of an in-process tracing ring buffer, no SSH required.
  • Updates page — version and build provenance (git SHA, build date), bundled release notes, optional remote update check.
  • One-click restart — snapshots persist, the server exits cleanly, your supervisor relaunches it, and the UI reloads itself when health returns.
  • /v1/admin/metrics now reports a per-component storage breakdown, process RSS, and WAL event counts.

The UI is backed by a new authenticated internal API under /internal/ui/* (not part of the stable v1 surface; endpoints will be promoted to /v1 as they prove out).

  • Deterministic quantitative answers: when source material states a cumulative total, the answer stage returns it exactly instead of re-counting; count questions over a window with provably zero matches return an exact, explained zero.
  • Facts plane consolidation: a background fact consolidator and sweeper, question-shaped fact keys for cross-session attribute lookups, and grade-aware fact ranking.
  • Query-shape classifier: a new prototype + bandit classifier for recall/answer queries, shipping in shadow mode.
  • LLM gateway: multi-lane enrichment model pool with per-lane health rotation; Claude 5 adaptive-thinking support in the Anthropic router.

No storage migration — 0.9.x data directories work as-is. The /v1 API surface is unchanged. UI-driven overrides persist as two sidecar JSON files in the data directory.

v0.9.3

GDPR erasure integrity, audit hash chain, temporal phrases

v0.9.2

real-time webhook ingest fixed (connectors 0.2.13)

Maintenance release — no format changes, no migration: 0.9.0/0.9.1 data directories work as-is. The headline is cortexdb-connectors 0.2.13 (on PyPI now), which makes the real-time webhook path functional against real tl;dv and Jira, plus one server-side fix to the LLM answer router for Claude 5-family models.

Full notesHide notes4 notes

The serve webhook receiver previously verified every provider with GitHub's X-Hub-Signature-256 body-HMAC scheme. Real providers don't all sign that way, so real deliveries were rejected at the door. Both findings were validated against a real tl;dv workspace and a captured live delivery.

  • tl;dv deliveries are accepted (was: 100% rejected with 401). tl;dv doesn't HMAC-sign webhook bodies — its auth is a static custom header. Configure the tl;dv webhook (Settings → Webhooks) with custom header Authorization: Bearer <TLDV_WEBHOOK_SECRET>. Fail-closed when no secret is configured.
  • TranscriptReady keeps its transcript (was: keyed by the transcript id, transcript text dropped, timestamp = ingest time). The episode is now keyed by the meeting id, the segments are ingested as the transcript through the same builder as the poll path (so poll + webhook dedup), and chronology comes from the delivery's executedAt.
  • Jira signature verification reads the right header: Jira signs with the same sha256=<hex> HMAC but sends WebSub-style X-Hub-Signature, not GitHub's X-Hub-Signature-256. Correctly signed deliveries now verify.
  • Freshdesk moves to static-header auth (Freshdesk automations can't body-sign): configure the automation rule to send Authorization: Bearer <FRESHDESK_WEBHOOK_SECRET>.

Upgrade: pip install -U cortexdb-connectors. Action needed for tl;dv and Freshdesk webhooks: set the Authorization header in the provider UI as above. Jira and the poll/sync paths need no changes.

Claude 5 models enable extended thinking by default; on answer-sized token budgets the model could spend the entire budget thinking and return no text, which surfaced as blank completions and — under benchmark concurrency — tripped the answer provider-health circuit. The router now requests plain completions (thinking: disabled) from 5-family models; CORTEX_ANSWER_THINKING_BUDGET=N opts back into thinking with an explicit budget. Older model families are unaffected.

v0.9.1

connector fidelity release (connectors 0.2.12)

Maintenance release. The server is unchanged from 0.9.0 apart from the version stamp — no format changes, no migration: 0.9.0 data directories work as-is. The substance of this release is cortexdb-connectors 0.2.12 (on PyPI now), which fixes every finding from three external bug reports and a 14-report canary-fleet audit.

Full notesHide notes8 notes

tl;dv — the headline fixes, validated against real meetings:

  • Idempotency keys are content-versioned (tldv:{id}:{hash}): a meeting synced before its transcript was ready — or later re-transcribed or renamed — lands as a new version instead of becoming a permanent 409 that lost the transcript and failed every subsequent sync run.
  • Timestamps parse the API's real format (JS Date.toString()): meeting chronology is correct, --since incremental sync actually filters, and watch no longer re-pulls every meeting and transcript on every interval.
  • Visibility reflects the meeting's privacy: private meetings and 1:1s are labeled private/restricted with visible_to= principals — no more blanket organization on confidential content. Per-meeting scope isolation via {entity.meeting} scope templates.
  • Poll and webhook paths emit identical envelopes (no double-store); 429/5xx retries with backoff; a failing item dead-letters after 5 cycles instead of blocking the sync forever.

Confluence — comments are now ingested (they were silently dropped), bound to their host page by container (title-spoof-proof); page bodies land in full as clean text (previously ≤1,000 chars of raw XHTML with the search snippet preferred over the body).

Google Workspace — Gmail message bodies are now ingested (previously subject + snippet only); calendar recurrence expansion is bounded to 90 days out.

Jira — webhook 409 replays ack 200 instead of 500 (ends Jira redelivery storms); webhook ADF descriptions are flattened instead of dropped. Attachment byte-purge (DELETE /v1/blobs/{id}) works end-to-end on servers ≥ 0.9.0.

All connectors — surrogate-safe content, error messages that name the exception type, crash-proof watch loops.

Full details: connectors/CHANGELOG.md.

  • Server: docker pull cortexdb/cortexdb:0.9.1 — drop-in from 0.9.0. (Upgrading from 0.8.x? Read the 0.9.0 notes first: that release changed the on-disk format and the fresh-install defaults.)
  • Connectors: pip install -U cortexdb-connectors (0.2.12).
v0.9.0Breaking

new storage engine, benchmark-grade recall, vector-only defaults

⚠️ BREAKING RELEASE. 0.9.0 ships a new on-disk format and new engine internals. Data directories created by 0.8.x are not supported — start 0.9.0 on a fresh data directory (export what you need from 0.8.x and re-ingest). The store now carries a format-version gate that refuses mismatched generations fail-closed instead of corrupting silently. The fresh-install configuration default also changes: see "Vector-only by default" below.

Full notesHide notes11 notes

This is the largest CortexDB release to date: 372 commits merged from three long-running engineering campaigns — stress (storage engine + scale), tq2 (quantization + capacity), and openai (recall quality) — plus a release-day soak-validation pass that fixed four production defects at 20M+ vector scale.


  • One WAL. The dual write-ahead-log stack is gone. Captured events and derived artifacts now live in a single unified, append-only log — one source of truth, one replay path, deterministic rebuilds of every index. This is the core of the breaking on-disk format change.

  • Durable indexer tail. Indexing is a durable log consumer with a persisted floor: boot means "continue from the floor", crash recovery re-indexes at most one in-flight window, and a poison event is durably excluded instead of freezing ingestion. Bulk accepts measured at ~18,000 events/s aggregate on a 32-core box (~4× the dual-WAL stack).

  • Layer-store snapshots (A1). Episodes/beliefs/concepts snapshot to disk and resume from sequence watermarks; combined with tail-only vector reconcile and a bi-temporal boot snapshot, cold boot at 72M vectors went from ~80 minutes to under 2 minutes.

  • jemalloc everywhere. The Docker image and production guidance ship whole-process jemalloc with tuned decay — −55% RSS at scale versus glibc (63.5 GB vs ~140 GB at 72M vectors). (A per-column-family RocksDB cache-tiering experiment was measured net-negative and removed.)

  • Bounded background maintenance. The memory-state (methylation) sweep is index-driven and budget-bounded, and compaction's content-store scan is budget-capped with resume — write-stall spikes from background scans are gone (compaction pass ~40s → ~5s).

  • Validated at scale. The engine was soak-proven on a 72M-vector corpus (spot i4i.8xlarge): sustained mixed read/write load, graceful spot-reclaim handling, restart replay parity, and a measured capacity envelope (~2,400 indexed writes/s clean; read ceiling scales with corpus size).

  • TQ2 quantized hot tier (new default). In-memory vectors are stored ternary-quantized with full-precision rescoring on candidates. Quality was gate-checked against real embeddings at recall@10 = 1.000 vs the fp32 baseline.

  • fp16 vector rows on disk (new default). Halves vector row storage; all read paths decode through a single seam. Part of the breaking format change.

  • Cold tier + residency cap. CORTEX_VECTOR_RESIDENT_MAX bounds the hot tier; overflow demotes to an fp16 cold tier that still participates in search via rescoring. RAM stays flat as the corpus grows.

  • Single-write coalescing (default on) and TQ2-with-enrichment compatibility — the quantized stack is the production path, not a benchmark trick. Opt-outs exist for every default (CORTEX_HNSW_QUANTIZATION, CORTEX_VECTOR_FP16_ROWS, CORTEX_UNIFIED_WAL).

  • Recall correctness at bulk scale. The inline mirror is now the primary applier for bulk writes with the tail as an idempotent catch-up; a whole class of "frozen watermark" defects was eliminated with structural guards (registration obligations that cannot leak, in-flight age reclaim, and IndexerGauges — watermark/head/lag/inflight — on /v1/admin/metrics). Drain-complete now has an exact machine-checkable gate (lag==0 && inflight==0).

  • Read-path latency work. The WAL supplement is skipped entirely on a settled store, walks in small pages bounded by the indexed watermark otherwise, and query embeddings are front-cached — hundreds of milliseconds off every recall on quiescent stores.

  • Deterministic query-shape classifier routes question types without oracle input, and namespace isolation (ADR-MEM-007) guarantees derived artifacts can never displace raw evidence in ranked retrieval.

  • Provider reliability surfaced. Runtime provider health checks, freshness-gap records for provider outages (recoverable instead of silent enrichment loss), and transient embedding-transport retries.

  • LiteLLM gateway support for routing embeddings/enrichment through a local gateway with multi-key pools — cheap, fast, provider-agnostic.

  • Measured result: the targeted-recall gate hit 30/30 twice under a blind protocol, and the campaign's read path is what produced the published LongMemEval-S and LoCoMo numbers.

  • Parallel indexer drain. CORTEX_INDEXER_CONCURRENCY now drives concurrent page application in the tail (default: available cores). Measured 13–16× drain throughput at 21M vectors (~365 → ~6,000 events/s).

  • Recall memory blow-up fixed. A recovery path materialized the entire captured log per recall on large stores (OOM under concurrent load). It is now a hard-capped bounded walk; recalls at 21M vectors complete in ~45 ms.

  • Snapshot persists no longer stall traffic. HNSW shard snapshots serialize from a lock-free clone; the former ~60s rolling write/read stall sweep on every persist cycle is gone (worst write p95 during a full snapshot: 3.5 s → 62 ms).

  • 48-hour sustained soak on the release binary at 200 writes/s + 60 recalls/s over a 20M+ vector corpus: zero write errors, zero indexing failures, flat memory.

A fresh 0.9.0 install (binary or Docker) runs exactly the published benchmark configuration: hybrid vector+BM25 retrieval — and nothing else. Every subsystem beyond retrieval is now an explicit opt-in:

FeatureEnable with
Layer scheduler (episodes/beliefs/concepts)CORTEX_V1_LAYERS_AUTO=1
Entity graphCORTEX_ENTITY_GRAPH=1
Enrichment / fact extractionCORTEX_ENRICHMENT_{URL,MODEL} + CORTEX_ENRICHMENT_DELAY_SECONDS
Artifact routesper-route flags (see docs)

If you relied on the 0.8.x default-on layer scheduler or the Docker image's baked-in enrichment config, set the flags above explicitly. With nothing set, CortexDB makes zero background LLM calls.

  • /v1/admin/health and telemetry report the real release version (goodbye hardcoded 0.1.0).
  • GET /v1/admin/flush-views is retired — graceful shutdown (SIGTERM) runs the final snapshot persist.
  • Deep index repair re-chunks from surviving WAL content, closing a mid-crash recovery gap; POST /v1/admin/index-audit gained vector auditing ({"vectors": true}).
  • Store format-version gate: a newer-generation data directory is refused fail-closed instead of half-opening.
  • SDKs (cortexdbai on PyPI/npm), the MCP server, connectors, and the CLI are unchanged and fully compatible — the v1 API surface did not change in this release.
  1. Do not point 0.9.0 at a 0.8.x data directory. Start fresh and re-ingest (the WAL format, vector row format, and quantization defaults all changed).
  2. Review the vector-only defaults table above and explicitly enable the features your deployment uses.
  3. docker pull cortexdb/cortexdb:0.9.0 (or :latest); binaries for linux-amd64/arm64 are on the releases page with sha256 checksums.
v0.8 series
22 releasesJuly 15, 2026 – July 27, 2026
v0.8.21

vector-aware index audit

POST /v1/admin/index-audit accepts {"vectors": true} (default false, so existing behaviour is unchanged).

Full notesHide notes4 notes

The audit's membership test is BM25-only by design: a cold-tier demotion removes a vector's in-memory graph node while the vector itself stays intact and searchable, so treating "absent from the graph" as missing would flag healthy consolidated data as lost.

The gap that leaves is an embedding outage. The fulltext document is written whether or not embedding succeeded, so events from that window are keyword- findable but have no vector at all — invisible to semantic recall, and reported clean by the audit. Repairing their fulltext even makes them harder to find, because they then pass the BM25 check.

With vectors: true the audit asks whether a stored row exists in EITHER tier, which separates "demoted" from "truly gone", and with repair: true re-embeds the lost vectors from stored content. New counts are reported separately (missing_vectors, missing_vectors_total, vectors_repaired), so existing consumers are unaffected.

Operators: fix a broken embedding key BEFORE repairing, or every chunk lands in vector_repair_skipped instead of being restored.

v0.8.20

sibling-scope idempotency, blob deletion

Two correctness fixes from an external connector report, plus the cortexdb-connectors 0.2.11 Jira release that depends on them.

Full notesHide notes4 notes

Idempotency is namespaced by the full scope path. It was namespaced by the scope root while the request body it is compared against carries the full scope — so writing the same item into two sibling scopes under one tenant (org:acme/project:a and org:acme/project:b) looked like one key reused with a different body and returned 409 IDEMPOTENCY_CONFLICT. Anything syncing one source into several scopes silently stored only the first. Tenant isolation is unchanged, and a retry that spans the upgrade still replays its original event id for the remainder of the 24h key TTL.

GET /v1/experience/status?scope= now means the scope the write named; the scope-less form still searches the caller's own namespaces.

DELETE /v1/blobs/{blob_id}. Uploaded objects had no delete path, so a file deleted at its source stayed byte-retrievable no matter what happened to the experience referencing it — redacting the event removes the memory, not the object. Authorized by ownership exactly like GET (another owner's blob answers 404), idempotent, and mirrored in the Python and TypeScript SDKs.

Known limitation: POST /v1/forget removes events from GET /v1/events but they remain reachable through POST /v1/recall. Deleting uploaded files is fully effective (the bytes are purged); redacting text events is not yet. Fix in progress.

v0.8.19

recency-superlative recall — 'my last mail'-style queries now select the newest in-scope events (recency supplement channel) and rank them chronologically; micro30 parity 28/30

v0.8.18

content processors decoupled from enrichment — tika/unstructured/vision/ASR extraction now runs with enrichment off (content-only scanner); bulk blob_ref items extract; scanner commit race fixed (no duplicate processor calls / derived-text index entries)

v0.8.17

NFC-canonicalized idempotency (KAN-21) — canonically-equivalent re-sends replay instead of 409; NFD/NFC key spellings dedup; upgrade-safe via legacy-hash acceptance

v0.8.16

scoped recall performance, background compaction pacing, conflicts API subtree queries

  • Scoped WAL listing: recall's durable-WAL supplement now reads only the partitions of scopes matching the requested prefix instead of scanning the entire WAL. Deployments with many top-level scopes sharing one instance see order-of-magnitude faster holistic/descend recalls (measured 264s → seconds on a 300-scope corpus).
  • Background compaction pacing: a compaction pass that outruns its configured interval no longer re-fires immediately; the scheduler now idles at least as long as the pass took (≤50% duty cycle) and logs a warning suggesting a larger scheduler.compaction_interval_secs. Fixes sustained full-core CPU usage on large corpora.
  • Fixed the evolution-stats facts counter inflating by the examined edge count on every compaction pass.
  • GET /v1/conflicts is now subtree-inclusive: scope=org:acme returns conflicts for the org root and every descendant scope (path-segment boundaries, no string-prefix leakage). The built-in status page uses this to issue one request per scope root instead of one per scope.
  • Partial cortex.toml files now parse: every configuration section is optional, so a file carrying only the tables you want to override (e.g. [scheduler]) works as documented.
  • New docs: scope-layout guidance for multi-tenant deployments (prefer org-rooted scopes).
v0.8.15

context_block always serializes

StratifiedPack.context_block is a documented field and now serializes even when empty (was skip-if-empty, which made the key vanish exactly in the no-context case — KeyError in every bracket-access consumer, including the published LangGraph integration example). Cookbook + website examples switched to safe access and include events in recall include lists.

v0.8.14

layer observability

GET /v1/admin/layers/stats health gains: llm_roles (the four LLM cost centers separated; entity extraction explicitly rule-based/no-LLM), layer_sources (per-layer derivation source + current gate, incl. facts=deterministic_triples_only when no enrichment model), and enrichment_backlog_stalled (deferred backlog that can never drain because CORTEX_ENRICHMENT_MODEL is unset). Additive JSON only.

v0.8.13

scope migration workflow

POST /v1/admin/scopes/migrate — first-class copy/move of a scope's memory into another scope (the vault-migration report's unified-scope ask). Resumable and duplicate-free via deterministic v7 destination ids; event_time/content/metadata preserved; [triple] events regenerate deterministic facts under the target; mode=move erases the source via the standard scope-wide forget only after a clean count-verified copy and explicit confirm_erase_source=true. Gated on admin.compact.

v0.8.12

derivation control + concept dedup core

  • extract:[] events are excluded from concept reflection — raw-index-only now holds across every derived layer (runtime/tool metadata no longer becomes durable personal concepts)
  • concept dedup: stable noun-phrase titles (generation questions stay internal), per-concept EVIDENCE support sets, and semantic upsert (name match → embedding cosine >= CORTEX_CONCEPT_UPSERT_SIM default 0.86 → in-batch sibling dedup) so synthesis passes update canonical concepts instead of inserting question-variants
v0.8.11

bulk wait=captured (raw-first ingest), include_vectors=false default, concept lineage fix

  • POST /v1/experience/bulk?wait=captured: ack at WAL-durable capture, batched mirror (embedding + indexing + triple facts) continues in background under admission control — the raw-first/derive-later mode for large backfills
  • include_vectors=false default on /v1/facts, /v1/beliefs, /v1/understanding[/{id}] (summary_embedding omitted unless requested)
  • concept previous_version no longer self-references on version bumps; legacy self-loops scrubbed on write-through
v0.8.10

E5 bitemporal validity

  • out-of-order backfills are retro-closed: an older observation arriving after a newer same-key fact gets its valid_to closed at the nearest newer fact's valid_from (replay-durable via FactSuperseded), so point-in-time queries return exactly one truth per instant instead of N co-current values
  • recall pinned to a past as_of_valid now sees the superseded fact that was true at that instant (the recall-side recurrence of DISC-025); unpinned current-state recall unchanged
  • micro30 parity A/B vs v0.8.9 baseline: identical scores and identical per-question verdicts (benchmark-neutral)
v0.8.9

fact-erasure durability

  • E4 (HIGH): erased facts no longer resurrect on unclean shutdown — every fact-deletion path redacts the fact's WAL entries (content gone from the log, mirroring the event layer's erasure closure)
  • D3: reference-counted selector erasure — a fact survives on remaining supports with reduced provenance (replay-durable); only full coverage deletes
  • Boot reconciliation drops facts whose every supporting event is erased and redacts their entries — also cleans corpora where pre-v0.8.9 replay (incl. the v0.8.7 snapshot-upgrade full replay) already resurrected erased facts; kill switch CORTEX_FACT_ERASURE_RECONCILE=0
v0.8.8

signup-token confinement fix (cloud-canary outage) + per-partition WAL truncate

  • signup tokens now confine capabilities to BOTH the org-prefixed home scope and the bare actor scope — v0.8.5's A5 enforcement had left signup users able to write at user:u_X but 403'd on capability-gated reads there (view=holistic), red cloud-canary since 2026-07-16
  • WAL truncate_below_sequence fails closed on multi-partition WALs; new per-partition truncate_below_floors (the 'truncate seq' item deferred by the v0.8.5 audit, same class as the E1 replay floor)
v0.8.7

external v0.8.4 crash-test findings (E1 fact durability, E2 bulk triples, E3 bulk stranding) + wait-mode ergonomics

  • E1 (HIGH): facts no longer lost on unclean shutdown — FactStore snapshot v2 with per-partition WAL replay floors; old snapshots trigger one full replay on upgrade (no operator action needed)
  • E2: /v1/experience/bulk now runs the deterministic direct-fact path for content.kind=triple (one batched embedding call)
  • E3: failed bulk batches no longer strand acked events; index-audit repair rebuilds content rows from WAL event content; index-audit honors scope
  • API: wait=accepted accepted as async-default alias; 422 lists valid wait values; openapi.json wait enums corrected; context.labels documented as the app-metadata extension point
v0.8.6

recency retrieval + answer API contract fixes

New

  • order: "recency" on /v1/recall and /v1/answer — timeline-tail retrieval that serves the newest-K events by event time instead of relevance ranking. Fixes "recent / latest / currently…" questions on repetitive histories, where ranking favored the most-repeated items and could miss the newest ones entirely. Tail size comes from budgets.per_layer_limits.events (else CORTEX_RECENCY_TAIL_EVENTS, default 100). /v1/answer infers recency ordering from question phrasing; an explicit order always wins. Kill-switches: CORTEX_RECENCY_ORDER=0 (mode), CORTEX_RECENCY_INFERENCE=0 (inference).
  • CORTEX_ANSWER_DEFAULT_INSTRUCTIONS — a deployment-wide standing prompt addendum for /v1/answer and /v1/compose (for example "always include entity identifiers in answers"). Per-request instructions still apply and can refine it.
Full notesHide notes1 note

Fixed

  • /v1/answer now honors its documented budgets and include fields; both were previously dropped on deserialization, so callers could not cap evidence size or whitelist layers.
  • Layer backfills now respect the request's include whitelist. Previously an include:["events"] answer could re-acquire episodes through the backfill path, bypassing modality filters — filtered-out conversation content could surface in events-only answers.
  • API reference: documented the /v1/recall order parameter and corrected the /v1/experience/bulk request shape (scope is per item; there is no batch-level scope/actor).
v0.8.5

deep-audit hardening + object_store bump

Resolves the 2 CRITICAL, 5 HIGH, and 2 MEDIUM findings from an adversarial multi-agent audit of v0.8.4, plus a security dependency bump.

Full notesHide notes2 notes

CRITICAL: POST /v1/scopes register scope-ownership takeover (A1); /v1/erasures attribute-selector whole-scope wipe (A2). HIGH: /v1/forget idempotency+preceded_by leaks (A3); /v1/answer missing view-capability check (A4); token scopes confinement never enforced (A5); blob Pending-forever with enrichment off (A6); mirror-failed writes resurrected on restart (A8). MEDIUM: experience-read blob owner-ACL bypass (#10); DELETE /v1/scopes?records=forget silent no-op (#12). SECURITY: object_store 0.11->0.14 pulls quick-xml 0.41, fixing RUSTSEC-2026-0194/0195 for real (#28).

Deferred to a focused follow-up: multi-partition FactStore/WAL sequence handling (A7) and minor envelope polish.

v0.8.4

erasure reference reconciliation + recall stream honesty

Fixes the v0.8.3 external report. Three findings shared one root cause: erasure deleted the event but left references to it behind.

Full notesHide notes2 notes
  • D1 (HIGH): erasing an event left its idempotency record in place, so a later re-write with the same key replayed the erased id and stored nothing — silent permanent data loss surviving restart. The record is now swept (memory + persistent) on erasure so the key re-processes.
  • D3: an identical (subject,predicate,object) re-assertion replaced the fact and dropped the prior asserter, letting erasure destroy a claim other events still assert. Identical triples now accumulate provenance (append to supports) instead of superseding.
  • D2: dangling context.preceded_by refs to erased events are scrubbed from surviving records.
  • C1: /v1/recall now 422s stream:true (pointing to /v1/recall/stream) instead of silently ignoring it.

No behavior change for normal usage; all prior regression checks pass.

v0.8.3

Wave 3: features, validation, honesty

The feature-grade and polish tail of the v0.8.0 audit. No CRITICAL or HIGH severity findings remain open after this release.

Full notesHide notes4 notes

Features

  • Durable metadata (H3): vocabularies, temporal phrases, and blob metadata now persist to JSON sidecars and survive restart; GET /v1/blobs/{id} serves bytes from the durable blob store on a cache miss.
  • Multimodal terminal status (H5): blob content whose modality has no configured processor is marked Failed at write time instead of sitting Pending forever.
  • TTL honesty (H2): removed a dead cleanup stub; the belief-layer TTL is now validated. (Auto-expiry sweep tracked as a follow-up feature.)

Validation consistency

  • limit=0 is clamped to >=1 across all offset-paginated listings (was an infinite-loop trap for clients paging to completion).
  • Conflict routes now emit the standard flat {error_code,message,retriable} error envelope like every other route.
  • A malformed page cursor returns 422 INVALID_CURSOR instead of silently serving page 1.

Honesty & docs

  • Stub (no-LLM) answers disclose model "stub-no-llm-configured" instead of echoing the caller's requested model in diagnostics.
  • API_REFERENCE_V1 reconciled with the shipped routes — removed endpoints that 404 (policy PUT/tenant/scope/actor, the erasure preview-manifest endpoint) and corrected admin operation names.

Security (advisory-DB drift since v0.8.2)

  • Updated crossbeam-epoch 0.9.18->0.9.20 (RUSTSEC-2026-0204) and memmap2 0.9.10->0.9.11 (RUSTSEC-2026-0186) — both real fixes.
  • Documented-ignore of two quick-xml DoS advisories (RUSTSEC-2026-0194/ 0195): reachable only via trusted cloud-provider XML, not client input; proper fix (object_store bump) tracked separately.
v0.8.2

DATA-SAFETY: selective erasure fix (upgrade advised for anyone using /v1/erasures)

  • POST /v1/erasures erased the ENTIRE scope, ignoring the selector. The request carried no selector field, so a caller-supplied selector.memory_ids was silently dropped and the whole scope was permanently, irreversibly destroyed — returning 202. An empty, absent, or typo'd selector all wiped the scope, on the very endpoint operators are told to use for GDPR erasure requests. /v1/erasures now honors selector.memory_ids (erasing ONLY the named records and their derived data), requires confirm_all: true for a deliberate whole-scope erasure (matching /v1/forget), rejects a typo'd selector key with 422, and reports the true deleted count. A GDPR request targeting one subject no longer destroys the tenant.
Full notesHide notes2 notes
  • Cluster mode silently disabled ~half the API. The distributed coordinator implemented only 8 of 75 storage methods and fell through to inert defaults for the rest — so on a multi-node deployment, layer reads returned empty for locally-owned data, fact/selective deletion no-op'd, GDPR forget-tombstones were not durable (erased events resurrected on restart), usage/billing reported zero, and the index-audit always reported "clean". All storage operations now execute against the local engine; a delegation-completeness test prevents recurrence.

  • Back-compat: top-level metadata is accepted again. Pre-v0.8.0 clients that tagged events with a top-level metadata: {labels: [...]} object were hard-broken by the v0.8.0 strict envelope (422). That shape is accepted again and folded into context.labels. New clients should write context.labels directly.

  • Strict unknown-field rejection now covers /v1/recall, /v1/forget, the bulk request envelope, and budgets — a typo'd top-level key (e.g. temporl for temporal) returns 422 instead of silently dropping the temporal pin and answering with current-time data.

  • directives rejects unknown keys and validates ttl_for_belief_layer as an ISO-8601 duration (a typo no longer silently skips the directive).

  • Triple predicate is validated (non-empty, length-bounded) like subject/object ids.

  • CORTEX_EXTRACTION_BATCH_EVENTS startup logging corrected — the knob was always wired, but the log printed an unrelated fixed constant, making it look dead.

v0.8.1

SECURITY: cross-tenant authorization fix (upgrade advised for token-auth deployments)

  • Cross-tenant authorization on scope-supplied routes. Several endpoints acted on the caller-supplied scope with no membership check, while their read/write siblings (recall, events, by-id reads) correctly enforced scope-membership roles. On deployments using per-actor token auth (PASETO/JWT via the minter), any authenticated caller could target another tenant''s scope — one that tenant had written to (and thus auto-registered) — to delete/erase its data (/v1/forget, /v1/erasures), export its memories (/v1/export), take over the scope by rewriting its member roster (PUT /v1/scopes/members), delete/enumerate registrations (/v1/scopes, /v1/scopes/prune), revoke another caller''s token (/v1/auth/revoke), or read cross-tenant fact timelines (/v1/facts/timeline). All of these now enforce the same owner/reader membership checks as the rest of the surface. Deployments running with auth disabled, or with only the static operator key, were never exposed (the operator key is a deployment-wide secret by design, not a per-actor credential). Token-auth deployments should upgrade.
Full notesHide notes2 notes
  • docker run -v ./data:/data failed at first boot with Permission denied (a host bind-mount is root-owned while the server runs as uid 1000). The container now chowns the data directory at startup (via a privilege-dropping entrypoint) so the documented Docker quickstart works out of the box. docker run --user <uid> continues to work unchanged.

  • Ingestion connectors (Slack, Jira, Notion, and every connector using the shared framework) were broken by the v0.8.0 strict write envelope: they emitted context.thread_id / meta / preceded_by, which now return 422 and stalled the sync cursor. This metadata now rides context.labels. Connector insight detection was rewired off the retired /v1/entities route onto /v1/facts.

  • /v1/answer temporal valid_during — a query with only a valid_during window was treated as un-pinned, so derived-layer context was injected without applying the requested window. It is now a proper temporal pin, and the response as_of reflects the window.

  • CLI: forget --query now works (was silently dropped → confusing 422); bulk writes warn on a 207 partial commit instead of reporting success; --ordering help no longer promises unimplemented enforcement.

  • MCP: erasure dry-run now reports the real affected count (was always 0).

  • Python SDK README forget() example corrected (reason=); TypeScript bulk response type now surfaces results / failures / partial.

  • GETTING_STARTED recall example uses budgets.max_tokens (the prior tokens_total was silently ignored) and the correct Cortex SDK class name. API reference: context.observed_at and idempotency_key corrected to optional; idempotency replay is signaled by a response body flag, not a header.

v0.8.0

strict write envelope, honest bulk semantics, unified idempotency, fact-forget repair

  • The write envelope (POST /v1/experience, bulk items) is now strict: unknown or misplaced top-level and context fields return 422 instead of being silently ignored. Clients that were unknowingly sending typo'd or misplaced fields (most commonly observed_at at the top level, which silently lost the occurrence timestamp) will now get a clear validation error. As part of this, top-level observed_at is accepted as a first-class, validated alias for context.observed_at — the common placement now works correctly instead of corrupting the bitemporal timeline.
  • POST /v1/experience/bulk responses changed: accepted and event_ids now count distinct persisted events (idempotency-deduplicated items are no longer double-counted), and a new index-aligned results[] array reports each item's event_id and replayed_from_idempotency. A batch where some items committed and some failed returns 207 Multi-Status (partial: true, per-index failures[]); any 4xx/5xx bulk response now guarantees nothing was committed — whole-batch retries are safe.
Full notesHide notes2 notes
  • Selector-scoped fact deletion now works on single-node deployments. Since v0.7.5, five coordinator methods (get_fact, delete_derived_by_source, delete_derived_facts, delete_derived_by_attributes, delete_facts_in_scope) were served as inert defaults through the production wrapper — fact-targeted forgets silently deleted nothing. If you issued fact-selector forgets on v0.7.5–v0.7.9, re-run them on v0.8.0.

  • Bulk envelope validation is atomic: every item is validated before anything commits, matching the schema-error path.

  • Single and bulk experience writes share one idempotency bucket: the same (scope, key, body) replays across endpoints instead of double-storing; a different body under a reused key returns 409 from either endpoint. Import sources remain separate buckets.

  • Reusing an idempotency key with the same content but different context.labels is now reliably a 409 IDEMPOTENCY_CONFLICT (labels are part of the write identity), never a silent replay that drops the new labels.

  • CI gate repaired end to end (embedded-docs parity, TypeScript SDK suite actually executing, runner disk headroom) and the ci workflow is now a required merge check on main.

v0.7 series
10 releasesJuly 12, 2026 – July 15, 2026
v0.7.9

per-tenant idempotency keys and true point-in-time answers

  • Idempotency keys are now namespaced per tenant. The dedup scope is (scope root, caller, endpoint family, key), where the scope root is the top-level segment of the write's scope path (org:acct_46, user:alice). Two tenants reusing the same idempotency_key — even under one deployment API key — no longer collide: the second tenant's write is accepted normally instead of failing with 409 IDEMPOTENCY_CONFLICT, and a conflict response can no longer reveal another tenant's event_id. Within a tenant nothing changes: an identical retry still replays the original event, and reusing a key with a different body is still a 409. Upgrade note: idempotency records written by earlier versions do not carry across this upgrade — a retried pre-upgrade write re-processes (safely) instead of replaying.
  • POST /v1/answer now honors temporal.as_of (and the as_of_valid / as_of_recorded axis pins). Point-in-time questions are answered from the state at the pinned instant: retrieval, the evidence block, and the answer prompt all reflect the pin, and the response's as_of echoes the requested instant instead of the current time. Note that as_of pins both bi-temporal axes ("true at T" and "known at T") — use as_of_valid to ask what was true at T regardless of when the system learned it.
  • GET /v1/experience/status accepts an optional scope= parameter for an exact tenant-scoped probe by idempotency_key; without it, the probe searches all of the calling actor's own tenants (never another caller's).
  • Clearer validation errors:
    • An empty (or whitespace-only) content.text is rejected up front with 422 INVALID_ENVELOPE — previously it was accepted and then surfaced as 502 INDEXING_FAILED after the write was already durable.
    • Unsupported pagination parameters on POST /v1/recall (limit, offset, page, cursor, page_size) return 422 with guidance (use budgets to bound the pack; GET /v1/events to paginate raw records) instead of being silently ignored.
    • An empty filter value list (e.g. filters.metadata.labels: []) returns 422 instead of silently matching nothing — a footgun for dynamically-built filters.
Full notesHide notes1 note

Fixes the two remaining findings (and the minor validation gaps) from the v0.7.8 external bug report.

v0.7.8

complete-events packs keep their newest events under the default recall budget

  • Exhaustive and metadata-filtered recall packs no longer lose their newest events to the default token budget. The cross-layer knapsack (introduced in v0.7.6) applied its default 4,000-token budget to the events layer of complete-corpus packs and evicted from the tail — which, on a chronological corpus, deleted the newest events. With grounded citations enabled, the model could neither see nor cite the latest entries of a changelog-style corpus even though the context block carried them. Complete-events packs are now exempt from default-budget event eviction; derived layers still drain toward the budget, and an explicit budgets.max_tokens from the caller still applies to every layer.
Full notesHide notes1 note

This completes the fix started in v0.7.7: v0.7.7 corrected the citation renderer (recency window, byte-budget direction, marker parsing), and v0.7.8 corrects the upstream pack assembly so the renderer actually receives the newest events it is meant to protect.

v0.7.7

grounded citations keep the newest sources; consolidation controls for curated corpora

  • Grounded [S#] citations on exhaustive (whole-scope) packs now always include the newest sources: both the source-count window and the prompt byte budget trim from the oldest side of a chronological pack. Previously, "what is the latest X?" could be answered from the middle of a chronological corpus because the newest chunks fell outside the cited window even though retrieval had returned them.
  • Citation markers above the source cap now parse correctly. Recency windows can legitimately start above [S1], and a model citing the newest source in a large pack no longer has that citation silently dropped from the response.
Full notesHide notes1 note
  • CORTEX_ANSWER_MAX_CITED_SOURCES (default 20): per-deployment cap on the grounded-citation source window, for corpora whose answers legitimately draw on more than 20 chunks (for example, a full release history).
  • CORTEX_CONSOLIDATION_ENABLED (default on): first-class switch for background memory consolidation. Set 0, false, or off to disable it entirely — previously the only way to stop consolidation was to raise its thresholds out of reach.
  • Consolidation now skips curated content automatically: events written with context.intent: "documentation" or directives.extract: [] are never folded into summaries. Summaries of curated chunks go stale the moment the source documents change, and consolidation archives the sources it folds in — documentation corpora should never pay that trade.
  • Consolidation summaries now carry a source=consolidation label, making them queryable (GET /v1/events?labels=source=consolidation) and auditable instead of appearing as anonymous events in the scope.
v0.7.6Breaking

the 13-issue defect-report response: forget that forgets, fact history, deterministic recall, secure defaults

Closes every open finding from the v0.7.5 external report (DISC-006 through DISC-028):

Full notesHide notes3 notes
  • forget: fact ids and attribute selectors (about_subject / about_entity / predicate) now delete the derived records they name, with belief/concept cascade and accurate all-layer matched counts; a selector matching nothing says so explicitly. Superseded facts stay fetchable — GET /v1/facts/{id} is new, and as-of queries return the facts that were true then.
  • beliefs: genuine same-key conflicts (support_tier, plan, status, …) reconcile to stance:contradicted; per-fact cardinality metadata can mark any predicate single- or multi-valued.
  • deterministic recall: pack composition is now reproducible for a fixed query and store — derived-layer listings and rankers use total orderings instead of hash-map iteration order.
  • typed triples: deterministic writes are excluded from LLM enrichment (no more duplicated or hallucinated facts), and ?wait=consolidated resolves immediately for them; write status reports the furthest completed stage, consistent with stages_completed.
  • erasure: preview and completion counts now match the records actually deleted, including facts.
  • security: a keyless boot on a network-exposed bind (the Docker quickstart) generates and persists an API key on first start instead of serving an open store; CORTEX_INSECURE_NO_AUTH=1 is the explicit opt-in for open dev mode.
  • ollama: the native answer provider works out of the box (the default URL now targets Ollama's OpenAI-compatible /v1 surface; bare daemon addresses are normalized).
  • events: /v1/events and /v1/events/{id} return one canonical body for the same id.
  • performance: default embedding concurrency raised 4→8 and the write admission cap 64→128, recovering bulk-write throughput without touching the WAL fsync durability default.

Also included, disabled by default: fact-as-key retrieval expansion and question-shape-routed layer injection (CORTEX_FACT_INDEX_KEYS, CORTEX_LAYER_INJECTION_GATE) — bench-validated, awaiting production routing hardening.

No breaking changes.

v0.7.5Breaking

forget selector safety, idempotency under concurrency, enrichment robustness

Fixes for four issues reported against v0.7.4:

Full notesHide notes2 notes
  • forget: cascade=derived_only with a memory_ids selector now deletes exactly the derived records (facts, beliefs, episodes, concepts) traced to the selected events — never the whole layer. A whole-layer wipe requires an explicitly empty selector plus confirm_all:true. Deleted counts in the response are now accurate, and wiping a derived layer no longer touches event storage.
  • experience: concurrent writes with the same idempotency_key now collapse to a single event; retried and racing requests all receive the winning event_id. Applies to both single and bulk writes, and composes with the persistent idempotency store introduced in v0.7.4.
  • enrichment: entity extraction no longer silently contributes an empty graph on large, dense documents. The output cap is configurable (CORTEX_ENTITY_MAX_TOKENS, default 4096), truncated responses retry automatically at a higher cap, and internal provider limits no longer clamp dense fact extraction.
  • concepts: every concept now carries a walkable provenance trail — supported_by links the facts and beliefs that fed synthesis, and synthesis_inputs.layers reports only layers that actually contributed.

No breaking changes. All response-shape additions are backward compatible.

v0.7.4

recall integrity, index self-healing, ranking, and performance hardening

This release merges the recall-quality workstream on top of the v0.7.3 security patch. It closes a silent index-loss class, hardens recall scope/temporal correctness, adds index self-audit + repair, and lands cross-layer budgeting and de-crowding — validated to move the full LongMemEval-S server run up from the prior baseline with no regression.

Full notesHide notes5 notes

INDEX INTEGRITY (the headline)

  • Fixed a silent search-index loss: a derived-layer forget with an empty selector and confirm_all could strip a scope's BM25/vector documents while the WAL kept the events, so recall went blind on already-stored memories with no error. Derived-layer forgets now purge only derived state and never touch event documents.
  • New WAL<->index self-audit: the server can reconcile every durable event against the search indexes and repair any that are missing. Exposed as POST /v1/admin/index-audit (diagnostics.read; {"repair": bool}) and run report-only at startup (CORTEX_INDEX_AUDIT_STARTUP, default on). Filtered/exhaustive recalls now flag divergence between the listing and the ranked pool in diagnostics, so this failure mode can never be silent again.
  • Enrichment scanner: fixed a cursor-promotion bug that could mark un-applied events done across a restart, and a livelock that drained only one chunk per boot.

RECALL CORRECTNESS

  • Holistic recall at a member (leaf) scope no longer leaks sibling-member content; holistic/descend at a parent scope now blends member data upward (self + ancestors + strict descendants). Current-state answers no longer return a superseded belief when no as_of is given (bi-temporal as_of/valid-time queries unchanged).
  • Exhaustive recall no longer treats a scan-capped page as "drained" (no more silently-truncated exhaustive blocks); graph and sibling channels enforce scope isolation.

RANKING + BUDGETS

  • budgets.max_tokens is now a real cross-layer token budget (priority spend across layers, tail eviction, diagnostics.knapsack_evictions) — on by default; set CORTEX_CROSS_LAYER_KNAPSACK=0 to restore the prior behavior.
  • Per-document pack cap (default on): stops one multi-chunk document from flooding the pack on documentation corpora; a no-op on conversational memory. Optional exact-title topic boost and derived-layer embedding rerank ship default-off (CORTEX_TOPIC_MATCH_BOOST, CORTEX_DERIVED_EMBEDDINGS).

PERFORMANCE

  • Recall read path: removed a per-request O(corpus) WAL clone, a term-matching allocation storm, and a serialized retrieval stage; fixed a BM25 group-commit reader race. Warm p95 on a normal-sized corpus improved ~36%. Recall now emits per-phase timings in diagnostics.time_ms (recall.<phase>) for latency attribution. NOTE: on very large corpora under heavy concurrency the dominant recall cost is external calls (query embedding, reranker, query-expansion LLM) — the target of a follow-up performance pass.

UPGRADE NOTES

  • No storage-format changes; volumes migrate in place from v0.7.x.
  • The cross-layer knapsack and per-document cap are ON by default; both have kill switches (CORTEX_CROSS_LAYER_KNAPSACK=0, CORTEX_PACK_PER_DOC_CAP=0). Both were validated non-regressing on the eval corpora.
  • Includes all v0.7.3 security fixes (cross-tenant IDOR on by-ID routes, triple-enrichment integrity, extraction parsing).
v0.7.3

security: cross-tenant isolation, deterministic-fact integrity, recall scope correctness

This is a security and correctness patch. It closes a cross-tenant data-disclosure class on by-ID fetch routes, stops the direct-fact path from corrupting deterministic truth, and fixes recall scope resolution so member data neither leaks sideways nor goes missing.

Full notesHide notes5 notes

SECURITY — CROSS-TENANT ISOLATION (please upgrade)

  • Direct by-ID fetch routes (GET /v1/events/{id}, /v1/understanding/{id}, /v1/erasures/{id}, /v1/understanding/synthesize/{id}, the /v1/lifecycle by-id routes, /v1/blobs/{id}, /v1/import/{id}) resolved a record without a caller-supplied scope, so the scope authorization that /v1/recall enforces never ran. An authenticated caller could read another tenant's record by id. Every by-ID and direct-fetch route now performs object-level authorization against the resolved record's own scope (or, for scope-less staged objects like blobs and import jobs, against its owner). A caller with no access receives 404 — the endpoint never confirms that another tenant's id exists. The by-id write twins (erasure cancel, lifecycle memory-event cancel) authorize before mutating.

DETERMINISTIC-FACT INTEGRITY

  • Typed triples written through the synchronous direct-fact path are no longer also run through async LLM enrichment. Re-enriching an already-deterministic fact re-derived it from prose and could hallucinate adjacent attributes (an observed case turned a known-good plan=Pro into account_status=active). Triple-origin events are now skipped by the enrichment scanner.

ENTITY EXTRACTION (Anthropic and other providers)

  • Knowledge extraction no longer stalls at facts=0/beliefs=0 when the LLM wraps its JSON reply in markdown code fences or surrounds it with prose (which some providers do even when a JSON schema is requested), and no longer truncates large batched extractions. The parser now recovers the JSON payload from a fenced/prose reply on both the batched and per-event paths, and batches get a larger token budget. Completes the bug_03 series (v0.7.0 response_format type -> v0.7.1 json_schema strict field -> v0.7.3 lenient parsing + batch budget).

RECALL SCOPE + TEMPORAL CORRECTNESS

  • Holistic recall at a member (leaf) scope no longer pulls in sibling members' private content. Holistic resolution is now self + ancestors + strict descendants: a leaf has no descendants, so it can never surface a sibling (siblings are descendants of the shared parent, not of the leaf).
  • Holistic/descend recall at a household (parent) scope now blends member data upward (member beliefs and events surface at the parent) — previously these questions abstained even though the data existed one scope down. The two fixes are one model, so they cannot reopen each other.
  • Current-state answers (no as_of) no longer return a superseded value: when no time is pinned, recall keeps only currently-valid beliefs/facts (valid_to = null or in the future). Bi-temporal as_of / valid-time queries are unchanged — the guard is a no-op whenever a time pin is present.

UPGRADE NOTES

  • No storage-format changes; volumes migrate in place from v0.7.0–v0.7.2.
  • Behavior change to be aware of: a by-ID fetch of a record you cannot access now returns 404 instead of 200. Integrations that fetched cross-tenant ids (which should not have worked) will see 404.
  • If you self-host with auth disabled (dev_local) or a shared static API key, object-level authorization is not enforced (identities are operator-chosen) — this matches the existing membership model and is unchanged.
v0.7.2

forget safety, Anthropic-only extraction, honest filter docs

Three fixes from self-host evaluation reports against v0.7.1.

Full notesHide notes4 notes

FORGET SAFETY (critical)

  • POST /v1/forget with confirm_all: true sent alongside a non-empty selector used to run BOTH the targeted delete AND the scope-wide redaction — "delete these 3 records" silently wiped every event in the scope. The ambiguous combination is now refused with HTTP 400 AMBIGUOUS_SELECTOR_CONFIRM_ALL and an actionable message. Targeted deletes need only the selector (no confirm_all); scope-wide operations still require an EMPTY selector plus confirm_all: true, unchanged.
  • deleted.events no longer reports ~2x the true number (targeted deletes were double-counted through the storage propagation).
  • The forget response now carries requested and matched so callers can cross-check destructive results: matched < requested means some ids did not exist in the scope.

ANTHROPIC-ONLY DEPLOYMENTS

  • Entity extraction no longer 400s when CORTEX_LLM_URL points at Anthropic's OpenAI-compatible endpoint: the negotiated json_schema response format now includes the strict field Anthropic requires (response_format.json_schema.strict: Field required). Extractions that failed 6/6 with blocked_reason: enrichment_failing recover without re-ingest. Providers that reject the new shape degrade one rung further (format omitted), so no configuration can strand.

DOCS

  • The API reference's recall filters example was un-runnable (it pre-dated the shipped schema). The real shape is {"filters": {"metadata": {...}}} with exactly five attributes (observed_actor, caller, modality, labels, intent); attributes AND, list values OR, labels matches the exact stored string. The boolean/operator DSL was never in the binary and is no longer documented. Note the asymmetry: labels are STORED top-level in context.labels but FILTERED under filters.metadata.labels.

UPGRADE NOTES

  • If you scripted confirm_all: true together with selector.memory_ids, the call now returns 400 instead of deleting the whole scope. Drop confirm_all for targeted deletes.
  • No storage format changes; volumes migrate in place from v0.7.0/v0.7.1.
v0.7.1Breaking

durability, honest failure reporting, and a working out-of-the-box path

This release hardens the single-node engine end to end: acknowledged writes now survive power loss, forget/GDPR operations report failures instead of swallowing them, corrupt state fails loudly instead of silently becoming an empty database, and the documented quickstart works exactly as written.

Full notesHide notes6 notes

DURABILITY & CRASH SAFETY

  • WAL acknowledgments are fsync'd: an acknowledged write now survives power loss, not just process crashes. Benchmarks/bulk loads can opt into the faster non-synced mode with CORTEX_WAL_SYNC=0.
  • The v1 accept point is durable: ?wait=captured acks land on stable media before returning, idempotency records persist across restarts (a retry after a crash replays the same event id instead of re-processing), and events caught mid-crash are re-mirrored automatically on the next boot.
  • Every WAL read path now verifies checksums; corrupt rows are quarantined and reported instead of silently replayed. Fact snapshots carry an integrity digest — a truncated or tampered snapshot triggers a full rebuild instead of silently suppressing data.
  • Persisted index formats are now version-enveloped with automatic legacy fallback, so future format changes migrate cleanly instead of breaking existing data dirs.

FORGET / GDPR

  • Forgetting multiple memory ids now purges every id (previously only the first was physically deleted while the API reported success).
  • Targeted forgets purge all chunks of a multi-chunk memory, remove the memory from /v1/events listings, and delete associated blobs even for memories written before the last restart.
  • Backend failures during forget/erasure now FAIL the request with a retriable error and an audit id — deletion is never reported complete while copies remain. Deletes are idempotent, so retries are safe.

RECALL CORRECTNESS

  • Recall responses now honor the caller's contract end to end: layer includes, per-layer limits, exclude_content, and temporal windows apply to every result, including cross-layer and lineage injections.
  • Degraded recalls are visible on the wire: diagnostics carry _partial: true and degraded_sources naming what failed, so a backend outage is distinguishable from an empty corpus.
  • Fixed three inverted similarity comparisons that made write-path dedup treat maximally-DISSIMILAR facts as duplicates.

SAFETY & OPERATIONS

  • CORTEX_API_KEY is now a real credential: its value authenticates as a bearer token (constant-time compared). With a key set, anonymous signup is disabled. Without one, the server runs in an explicit local no-auth mode bound to loopback.
  • New readiness probe GET /v1/admin/ready checks data-dir writability, storage handles, and provider state (and reports degraded=true on mock-embedding corpora). Container healthchecks now use it.
  • Embedding provider identity is pinned to the data dir: a lost API key can no longer silently downgrade a real corpus to mock vectors — the server refuses to start with a clear re-index message. CORTEX_EMBEDDING_PROVIDER=mock is the explicit dev mode.
  • Corrupt vector-index state now fails startup and preserves the on-disk files for repair; it is never replaced by a silent empty index.
  • Verified cold backup/restore: scripts/cold_backup.py archives a stopped data dir with per-file SHA-256 manifests and restores only after full verification (tampered/truncated archives are refused). An end-to-end drill validates ids, recall, and forget markers across the round trip.
  • Anthropic-only deployments: entity extraction no longer 400s on the Anthropic /v1/chat/completions endpoint — the LLM router negotiates the response_format shape per provider at runtime.

PACKAGING & DOCS

  • docker compose up now starts the supported single-node deployment (no shipped default API key; loopback-published by default). The experimental cluster topology moved to docker-compose.cluster-experimental.yml and is clearly labeled non-operational.
  • The Docker build uses cargo-chef dependency caching and no longer masks build failures.
  • Quickstart, API reference, SDK defaults (port 3141), preset names, and reranker configuration now match the runtime exactly; docs served by the binary are byte-identical to the repo and are secret-scanned before shipping.
  • The API reference no longer claims unimplemented features (boolean filter trees, cross-layer token knapsack, backup endpoints, TTL auto-expiry); TTL inputs now log a clear not-enforced warning.

UPGRADE NOTES

  • First boot after upgrading writes an embedding-provider pin into the data dir (one log line; no action needed). If you later change embedding provider/model/dims deliberately, set CORTEX_EMBEDDING_ALLOW_REPIN=1 for one restart after re-indexing.
  • WAL writes are now synced by default; if ingestion throughput matters more than power-loss safety on a given host, set CORTEX_WAL_SYNC=0.
  • Forget/erasure APIs can now return 502 FORGET_BACKEND_FAILED where they previously reported false success — treat it as retriable.
  • Clients that parsed the recall filters docs literally: the accepted shape is the five metadata attributes (observed_actor, subject, modality, labels, intent); boolean AND/OR trees were never accepted and are no longer documented.
v0.7.0

bi-temporal recall for real, deterministic facts, and honest operations

The headline: the three deep external audits of v0.6.9 (19-item readiness review, 8-item personal-vault audit, self-host defect report) are fully closed in this release — every finding fixed or formally dispositioned, each fix verified live before shipping.

Full notesHide notes8 notes

BI-TEMPORAL RECALL

  • temporal.as_of_valid and temporal.as_of_recorded are now independent query axes: ask "what was true on June 1, as we knew it on June 15" directly. as_of still pins both.
  • Recorded-time and valid-time range filters now apply to every layer — facts, beliefs, episodes, and concepts all honor the same temporal predicates. A miss-range returns empty across the board instead of leaking out-of-window records.
  • Date-only range endpoints (2026-07-01) are accepted and end-inclusive; unknown temporal keys are rejected with a 422 instead of silently ignored.

DETERMINISTIC FACTS

  • content.kind: "triple" writes a fact directly — no LLM extractor in the loop. Load known-good facts with exact subject/predicate/object and they are queryable immediately.
  • Predicates are canonicalized against the ontology at fact-minting time (raw surface form preserved in metadata, plus a predicate_mapped coverage flag). Extraction now steers to one canonical vocabulary, so "billing contact" and "billing contact for" land on one queryable key.
  • Python SDK 0.10.0 and TypeScript SDK 0.8.0 support triple writes and the new temporal axes natively.

TRUSTWORTHY WRITE SEMANTICS

  • ?wait=consolidated is now a true barrier: it returns only after enrichment for that event is durably applied.
  • The write ack's event_id is the stored id — fetchable and citable immediately.
  • Idempotent replays are visible: acks and status responses carry replayed_from_idempotency.

ERASURE THAT STICKS (GDPR)

  • Scope-wide forget tombstones are now durably persisted and honored by every read path, including the raw event listings the background episode/concept builders consume — previously a rebuild running seconds after a forget could resurrect erased content into a fresh derived record, and tombstones did not survive restarts. Caught by the live deployment suite; fixed at three layers (durable write, listing filter, write-time race guard) and covered by a repetition test.
  • A structural fix rode along: the server's coordinator wrapper now delegates every trait method, with a build-failing enforcement test — this class of silently disabled feature (forget tombstones, parent-document hydration, cognitive-state persistence, importance decay, admin compact) cannot recur.

PREDICTABLE LLM SPEND

  • Enrichment unset now means zero background LLM calls, exactly as documented: background concept reflection follows the enrichment switch (CORTEX_V1_LAYERS_AUTO=1 explicitly opts events-only reflection back in; manual synthesize is never gated). layers/stats reports the live verdict under health.concepts_gated_by.
  • Models that reject the temperature parameter (newer reasoning-generation models) are handled automatically — version-aware defaults plus a self-healing retry that remembers the verdict per model.
  • The Ollama answer path works out of the box: bare :11434 URLs are normalized and no cloud key is required. Answer-provider failures surface as proper 502 errors, never as prose in the answer body.

OPERATIONS

  • /v1/admin/version reports the exact git commit and build date for Docker images and release binaries alike.
  • GET /v1/usage — durable per-scope recall counters for dashboards.
  • /v1/openapi.json serves the machine-readable API contract; the route inventory in the docs is generated from the live router.
  • Secure-by-default binds: an auth-disabled bare-metal start binds to localhost unless explicitly overridden (Docker images keep publishing on the container interface as before).
  • Event listing pages reliably through artifact-heavy stores; modality round-trips verbatim; the synthesis scheduler heartbeats even when idle so "alive but idle" is distinguishable from "stuck".
  • dev_local single-user deployments no longer require scope-membership bookkeeping; parent-scope answer/compose automatically descend into child scopes when the parent holds no direct memories (capability-gated).

Known gaps (tracked, not silent)

  • The legacy enterprise admin surface (enterprise status, backup, legal hold, encryption rotation, residency, consent/DSAR) was removed in the May v1-only cutover and has not yet been rebuilt on v1; the corresponding config profile feature-gates are parsed but not enforced. The live-suite tests for that surface are retained, skipped-with-reason, as the spec for the v1 replacement.

Upgrade notes

  • No storage migration required; new column families are created on first open.
  • If you relied on background concept reflection with enrichment disabled, set CORTEX_V1_LAYERS_AUTO=1 — the new default gates that LLM spend off.
  • Clients: Python cortexdbai 0.10.0, TypeScript cortexdbai 0.8.0, MCP cortexdb-mcp 0.6.0.
v0.6 series
10 releasesJuly 7, 2026 – July 10, 2026
v0.6.9

metadata filters for recall

POST /v1/recall (and /v1/answer, /v1/compose) accept a new filters block — hard eligibility predicates over stored event attributes:

Full notesHide notes5 notes
{"filters": {"metadata": {"labels": "primary_inbox"}}}

Relevance decides ORDER; filters decide ELIGIBILITY: a candidate that fails the filter never enters the pack (or the answer context), no matter how well it ranks. No more stuffing tag names into the query text and hoping BM25 notices.

  • Keys: labels (containment in the event's labels), intent, modality, caller, observed_actor (equality). A list value means any-of; multiple keys AND.
  • Fail-closed by design: unknown filter keys are rejected with 422 — a typo'd filter can never silently return unfiltered results.
  • Applied at every event-producing path, including the rendered context block that grounds /v1/answer and /v1/compose.
  • Recall diagnostics report dropped_metadata_filter.

Also in this release: hydrated events now carry their original labels and intent (previously dropped in projection), and a workspace-wide test-hygiene pass.

Python SDK 0.9.0 (recall/answer/compose(..., filters=...)), TypeScript SDK 0.7.0 (RecallFilters on all three calls), CLI 0.4.0 (cortexdb recall --filter labels=primary_inbox, repeatable).

v0.6.8

searchable cold tier, the scope contract, and /v1/compose

Demoted vectors now stay fully searchable. CORTEX_EVICT_VECTORS_ON_ENRICH moves enriched events' chunk vectors out of RAM into an mmap'd cold tier that every search still scans exactly — demotion is now a pure memory/latency trade with zero recall loss. The OS page cache decides what stays resident. Benchmarked on a real 22k-vector index: identical latency to the pre-demotion baseline. Escape hatch: CORTEX_COLD_SEARCH=0.

Full notesHide notes5 notes
  • Cross-scope synthesis now persists at the nearest common ancestor: memories from user:you/source:chatgpt and user:you/source:whatsapp synthesize into knowledge at user:you instead of failing with "mixed-tenant provenance" warnings. The leftmost scope segment is the isolation boundary: synthesis never crosses tenant roots, and such skips are now a counted health signal (synthesis_cross_tenant_skips_total).
  • New view=lineage for POST /v1/recall: child-scope recall can opt in to ancestor-scoped derived knowledge its own data contributed to — gated by provenance intersection AND per-ancestor credential checks. Raw ancestor events never surface.
  • forget now cascades to ancestor-scoped derived artifacts via a provenance index — synthesis whose evidence you erased is tombstoned and regenerates from surviving sources.

The synthesis-permitted counterpart to /v1/answer: digests, reports, and overviews as structured markdown (headings, tables, mermaid diagrams), with grounded [S#] citations on by default. Synthesis across memories is the job; fabrication stays forbidden; thin evidence gets per-section "insufficient evidence" notes instead of a global abstention. Parameters: view (incl. lineage), time window, depth (standard|exhaustive), length (brief|standard|deep), section outline, free-text instructions. Available in the Python SDK (compose()), TypeScript SDK, CLI (cortexdb compose), and the Explorer UI's new Compose card.

  • CORTEX_ENRICHMENT_DELAY_SECONDS replaces the old realtime/deferred split: every enrichment now flows through the durable WAL-cursor scanner (N seconds cadence; 0 clamps to a 5s floor). A restart can no longer orphan queued enrichment. Legacy CORTEX_ENRICHMENT_DELAY_MINUTES still honored (x60).

  • CORTEX_ENRICHMENT_CONCURRENCY default raised 2 -> 16 (the old default made bulk backfills crawl at ~4 events/min).

  • /v1/admin/layers/stats now separates raw ingestion health from derived enrichment health: enrichment_status (off|healthy|degraded|failing), per-cause fallback counters (date grounding, batch timeouts, invalid LLM JSON, missing batch indexes), and raw_indexing_healthy.

  • context.intent="documentation" routes enrichment to a normative-claims extractor: product facts become memory, example blocks and placeholder names never do.

  • Documentation predicates (defaults_to, released_on, listens_on, ...) joined the builtin conflict ontology — competing normative claims across doc revisions now open conflicts for human review instead of silently coexisting.

  • Beliefs pagination implemented; layer stats counts no longer clip at 1,000.

Python SDK 0.8.0, TypeScript SDK 0.6.0, CLI 0.3.0 (all with compose). MCP server unchanged (0.5.1).

v0.6.7

Compaction dedup: unbounded background growth fixed

  • Duplicate abstraction artifacts eliminated — background compaction re-persisted every derived procedure, knowledge summary, and concept on every cycle, growing storage without bound, degrading recall precision, and re-billing LLM generations. A durable dedup registry now guarantees each derived artifact is persisted exactly once. Long-running deployments were accumulating hundreds of thousands of duplicates from a few thousand raw events.
  • Repair endpoint for existing stores — POST /v1/admin/compaction/dedup removes accumulated duplicates in place (keeps the original copy of each artifact, deletes the rest from the search indexes, and seeds the registry so they cannot return). Idempotent and safe on a live instance.
  • Honest compaction stats — the compaction log now reports newly persisted artifact counts instead of re-derived counts.
  • Knowledge synthesis also fingerprints its LLM input, so unchanged entity communities no longer trigger a fresh generation every cycle.
v0.6.6

Bulk ingestion fix for Unicode content, honest version reporting

  • Unicode chunking fix — bulk ingestion no longer fails on multi-byte characters (em-dashes, non-Latin text) in long boundary-free content such as exported chat transcripts. Chunk overlap now always lands on character boundaries.
  • One bad item no longer fails the batch — a chunking failure on a single item is contained to that item; the rest of the bulk request indexes normally and the failed item is reported individually.
  • GET /v1/admin/version implemented — the documented endpoint now returns release version, crate version, git SHA, and build date instead of 404.
  • Consistent version reporting — the startup banner and usage telemetry now report the real release version (previously both said "v0.1.0" regardless of release), matching /v1/admin/health and the image labels.
  • cortexdb-mcp 0.5.1 — the CLI --version output is now derived from the package version instead of a stale hardcoded string.
v0.6.5

Answer controls, grounded citations, forget safety

  • Answer style controls — POST /v1/answer accepts answer_instructions, a prompt addendum (up to 4000 chars) that controls style, verbosity, and output format ("be terse", "answer as markdown with curl examples", "respond in German") without affecting retrieval. A new question_type "docs-assistant" produces thorough, example-rich documentation-support answers.
  • Grounded citations — pass cite_sources: true and the answer cites [S#] source markers inline; the response citations array reflects exactly the sources the model used, with real event ids and full support strength, instead of a fixed top-of-pack list.
  • Forget safety — a memory_ids selector now deletes exactly the named events. Scope-wide deletion of derived layers (beliefs, episodes, understanding) requires naming the layer or confirm_all, and scope-wide redaction requires confirm_all; the API warns instead of over-deleting.
  • Recall reliability — writes made after a forget are recallable again (erasure tombstones respect their cutoff on every read path); recall context no longer starves on heavily-enriched topics; ranked recall hydrates events reliably across restarts.
  • Leaner responses — recall packs no longer carry internal embedding vectors (responses shrink by ~6 KB per fact/belief).
  • Blob upload contract enforced — POST /v1/blobs takes the raw file bytes with Content-Type; multipart/JSON wrappers now return 415 unsupported_media_type as documented, instead of being stored verbatim.
  • Deployment recall tuning (opt-in) — CORTEX_RECALL_DEFAULT_MAX_TOKENS (deployment-wide context budget), CORTEX_RECALL_RELEVANCE_FLOOR (drop weak context matches instead of budget-filling), CORTEX_RECALL_SUPERLATIVE_RECENCY ("latest/newest" questions prefer the newest evidence). All off by default. Startup now warns when COHERE_API_KEY is set but CORTEX_RERANKER_PROVIDER is not.
  • Chunking fix — Markdown numbered-list markers are no longer treated as sentence boundaries, so document chunks stop ending on a dangling "1.".
  • Python SDK 0.7.0 — answer(..., answer_instructions=..., cite_sources=True) on sync and async clients.
v0.6.4

Write reliability: status probe, async accept, honest indexing errors

Reliability and observability release driven by an external integration audit. All changes are additive — no breaking API changes, no data migration.

Full notesHide notes8 notes

Write reliability

  • GET /v1/experience/status — write-status probe by idempotency_key or event_id: was a write stored, is it queryable yet, did it fail? Turns an ambiguous client timeout into a safe resume: "not found" or "failed" always mean re-sending is safe (it re-processes, never duplicates).
  • ?wait=captured is now a true async accept — the response returns at the durable accept point (~10ms, flat in payload size) and indexing continues in the background: the fast path for large transcripts and chat snapshots. Default behavior is unchanged (fully indexed + read-your-writes when the response returns).
  • Honest indexing failures — a write whose indexing fails now returns 502 INDEXING_FAILED (retriable: true) instead of reporting success; its idempotency record is dropped so a retry re-processes instead of replaying. Bulk requests map per-item indexing failures back onto the response, and ?wait=indexed barriers fail fast instead of timing out on events that will never index.

Developer experience

  • Structured 413 — PAYLOAD_TOO_LARGE with details.max_body_bytes and remediation pointers (bulk / blobs); the limit is tunable via CORTEX_MAX_BODY_BYTES (default 32 MiB) and documented with recommended chunk sizes.
  • Field-level validation errors — 422 INVALID_BODY carries details.field_path (e.g. items[3].context.observed_at), the parser reason, and a pointer to the matching schema.
  • GET /v1/schemas — the normative JSON Schemas served by the binary itself, for client-side preflight validation.

Observability

  • /v1/admin/metrics now reports version, per-route p50/p95/p99 latency, errors by error_code, indexing-failure count, and the on-disk storage footprint. New Prometheus series: cortexdb_indexing_failures_total, cortexdb_errors_by_code_total.

Clients

  • Python SDK write_status(), TypeScript SDK writeStatus() + bulk wait option, MCP server memory_status tool.
v0.6.3Breaking default

3× write throughput, enrichment scheduling tiers, event-driven layers

Breaking default change: enrichment (facts / beliefs / concepts extraction) is now off unless CORTEX_ENRICHMENT_DELAY_MINUTES is set. Set it to 0 for the previous always-on realtime behavior, or N greater than 0 to batch-enrich every N minutes via a crash-safe WAL-cursor scanner. Deployments that upgrade without setting this variable stop extracting new facts; recall over raw memories is unaffected.

Full notesHide notes6 notes

Performance

  • Write path: ~50 → ~155 writes/s at high concurrency. BM25 group-commit removes the per-document fsync; a new embedding coalescer batches vectors with a tunable accumulation window and in-flight call count. Zero errors through 256 concurrent clients.
  • Event-driven derived layers — facts land ~5s and beliefs ~15s after a write in realtime mode (previously up to 90s of tick polling).

Enrichment scheduling

  • Deferred mode: a durable per-partition WAL cursor drives batch enrichment — kill-9 tested, at-least-once with dedup, and forgotten or redacted events are never re-enriched.
  • New tuning surface: CORTEX_ENRICHMENT_CONCURRENCY, CORTEX_ENRICHMENT_SCAN_CHUNK, CORTEX_ENRICHMENT_BACKFILL, CORTEX_BM25_COMMIT_INTERVAL_MS, CORTEX_EMBEDDING_COALESCE_WINDOW_MS, CORTEX_EMBEDDING_INFLIGHT.
  • Enrichment mode and deferred backlog are reported on /v1/admin/layers/stats.

Fixes

  • Concurrent bulk ?wait= barriers no longer time out with 408 — the stage handoff backpressures instead of dropping events.
  • recall / forget flush group-committed BM25, preserving read-your-writes and GDPR erasure guarantees.
v0.6.2

Bundled /status page

  • Every instance now serves a /status page showing live ingestion progress and a conflict-review queue — watch a backfill move through enrichment and resolve detected contradictions from the browser, no separate console needed.
v0.6.1

Ingestion progress gauges + client releases

  • Enrichment queued / completed / pending gauges on /v1/admin/layers/stats, so operators can see exactly how far a backfill has progressed.
  • Clients published with the new conflicts/claims surfaces: Python SDK cortexdbai 0.5.0, TypeScript SDK 0.4.0, MCP server cortexdb-mcp 0.4.0.
v0.6.0Milestone

Bi-temporal knowledge & conflict management

CortexDB now tracks two time axes for every derived fact — when it was true in the world (valid-time) and when the system learned it (record-time) — and detects, queues, and resolves conflicting knowledge on top of that model.

Full notesHide notes1 note
  • Bi-temporal records — facts carry valid_from/valid_to plus a record-time interval. Corrections never erase history: superseded records stay queryable as-of any past date.
  • Conflict detection, queue & resolution — contradicting claims are detected on ingest, auto-resolved when the dates make the answer unambiguous (temporal split, correction, source priority), and queued for review otherwise. New API: /v1/conflicts and /v1/claims (two-axis "what was true at X, as known at Y" queries).
  • GDPR erasure closure — erasure now redacts WAL payloads in place with checksum recompute, so replay can never resurrect erased content.
  • Semantic-delta versioning for beliefs and concepts — layers version on meaning changes, not on every re-synthesis.
  • Predicate ontology — canonical predicates with synonym mapping lifted extraction canonicalization coverage from 35% to 84% on the quality harness.
  • Fix: the maintenance scheduler no longer holds the global coordinator write lock across cycles (removes periodic write stalls).
  • Ships in shadow mode by default (CORTEX_BITEMPORAL_MODE=shadow: observe and measure); enforce is opt-in per deployment.
v0.5 series
12 releasesMay 20, 2026 – July 6, 2026
v0.5.11

Undated-relationship dating fix

  • Time-invariant relationships (kinship, identity) stated without a date are no longer stamped with the observation date as valid_from — an undated fact now stays undated instead of appearing to begin the day it was written down.
v0.5.10

Recall & artifact-quality hardening

  • New artifact-quality evaluation harness scoring facts, beliefs, episodes, concepts, and recall output — quality regressions are now measurable.
  • Extraction fixes: correct first-person attribution, correct valid_until dating, retry on empty extraction, and relative dates anchored to the event's own timestamp.
  • Concept synthesis: domain-focused questions, real provenance, no internal-id leaks, no invented content.
  • Recall: fail loud on embedding dimension mismatch; unified graph edge endpoints; human transcript speaker lines are no longer stripped from returned context.
v0.5.9

Bulk backfill builds the full memory stack

  • Fix: POST /v1/experience/bulk batches above the synchronous extraction cap now dispatch async enrichment per memory, so large historical backfills build facts, beliefs, episodes, and concepts instead of raw events only. Single writes and managed connectors were never affected.
v0.5.8

Multimodal ingestion, client transcripts, knowledge graph view

  • End-to-end file ingestion — upload via POST /v1/blobs, reference as blob_ref content, and the file is extracted, transcribed, or described, then indexed like any memory. Validated for text, PDF/Office (Apache Tika), archives, images (vision models), audio (ASR), and video (keyframes + audio transcript). Processors are opt-in per deployment.
  • Client-provided transcript fast path — send an authoritative transcript inline with the blob reference and CortexDB indexes it directly, skipping re-transcription; original bytes are retained as provenance.
  • recall(view="structured") returns a JSON-LD knowledge subgraph (entities, predicates, confidence) built from the recalled facts.
  • Fixes: ffmpeg bundled in the runtime image; image/audio processor URLs accept either the API base or the full endpoint.
v0.5.7

Operational polish: request IDs, honest health, idempotency 409

  • Health endpoints report the real release version instead of an internal crate version.
  • Every v1 error body carries a request_id, and every response gets a matching X-Request-Id header (client-supplied IDs are honored) — a correlation ID to quote in support requests.
  • Reusing an idempotency key with a materially different body returns 409 IDEMPOTENCY_CONFLICT. The conflict check hashes a stable write identity that excludes server-defaulted fields, so genuine retries and connector re-syncs still replay correctly.
v0.5.6

Scope inheritance + durable GDPR erasure

  • Holistic recall inherits ancestor scopes — knowledge written at an org or region scope is folded into child-scope recall with zero per-user duplication.
  • Durable erasure — GDPR/DPDP forget now removes events from every read path and survives process restart; derived indexes (search, vector, content, blobs) are physically purged.
  • The write envelope is fully forgiving: context is optional, and a minimal {scope, modality, content} experience is accepted.
  • Structured error envelopes on write endpoints; small API papercuts closed from a full docs-vs-deployed-code audit.
v0.5.5

Forgiving write API

  • POST /v1/experience no longer requires hand-built envelopes: bare {"text": ...} content, observed_at defaulting to now, and auto-generated idempotency keys.
  • Bulk accepts experiences as an alias for items; the granular recall view now populates context_block; new unauthenticated GET /v1/health.
  • Driven by a live pilot audit — backward compatible throughout.
v0.5.4

Recall & answer quality + 29 framework integrations

  • Shape-aware reranking — the reranker adapts to question shape (single-fact vs enumeration/aggregation), with coverage-seeking facet merge and fan-out for enumeration questions.
  • Fact-join multi-hop retrieval for questions that join across memories, plus a relevance-ranked entity-to-facts channel.
  • Premise-gated abstention — the answer stage pushes back on false-premise questions instead of confabulating, grounded in retrieved evidence.
  • Stratified evidence pack (belief sheets + fact ledger) passed to the answer stage; real token usage surfaced in /v1/answer diagnostics.
  • The Python SDK bundles 29 framework integrations as optional extras — pip install cortexdbai[crewai] (LangChain, LlamaIndex, CrewAI, LangGraph, Letta, AutoGen/AG2, Agno, DSPy, and more).
v0.5.3

Unified codebase + managed connectors

  • One build everywhere — the former enterprise edition's hybrid recall merged into main; a single binary now serves community and enterprise deployments.
  • Six managed connectors (Slack, GitHub, Jira, PagerDuty, Confluence and more) integrated across bulk, streaming, and webhook ingestion, with a no-credentials test harness.
  • Per-instance dashboard branding (CORTEX_INSTANCE_LABEL, default scope, auth labeling) and an observed_actor ingestion fix.
  • MCP server: configurable recall view via CORTEXDB_VIEW, corrected recall rendering, and a rewritten insights engine.
  • Event labels and intent are preserved through /v1/events.
v0.5.2Default change

Opt-out usage telemetry

Default change: the community binary now sends a small periodic anonymous usage beacon (install UUID, version, OS/arch, endpoint call counts and latencies, data size). Never sent: request/response bodies, query text, stored memory content, IPs, or scope names. Opt out with CORTEX_TELEMETRY=0 or DO_NOT_TRACK=1 — nothing is collected or transmitted.

Full notesHide notes1 note
  • The beacon is best-effort and entirely off the request path — it never adds latency, and a failed send is silently dropped.
  • Build hygiene: clippy correctness lint fixed; benchmark-specific markers removed from shipped source.
v0.5.1Breaking

One coherent API, a real admin console, on-prem first

Breaking: the legacy /v1/remember write path is retired — use POST /v1/experience. Legacy SDK clients are replaced by the realigned cortexdbai packages (0.3.0). No data migration required.

Full notesHide notes1 note
  • The /v1/* surface is now the only public API: write via experience, read via recall/answer, per-layer GET endpoints, and the admin surface.
  • Bundled admin console — all five memory layers with per-row drilldown, stance pills on beliefs, confidence on facts, cursor pagination, and an inline answer panel with citations and a raw evidence-pack inspector.
  • On-prem first — boots with no API key for local use (with a loud warning) and flips to enforced Bearer auth the moment a key is set. No cloud dependency, no phone-home.
  • Five-layer recall in the stratified pack; per-layer lexical ranking in community recall; concept synthesis merges instead of overwriting.
  • New /v1/admin/metrics: request/error/rate-limit counters + uptime. 17 findings from an external API audit resolved.
  • Native per-arch Docker builds cut release build time from ~5 hours to ~6 minutes; operations docs bundled in every tarball.
v0.5.0First release

Benchmark-proven defaults, baked in

  • First tagged release: the Docker image ships with the recall configuration proven on long-horizon memory benchmarks as its out-of-the-box defaults — what we benchmark is what you run.
Try it

Bring the workflow that gets it wrong.

One command starts a local instance with its own volume. The changelog carries every release behind it.