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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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".
- 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.
- 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").
- 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:
| Capability | What it adds | Request fields | Response fields |
|---|
blob_text_decode_v1 | Charset-safe decoding of text blobs | none (behaviour) | 422 BLOB_TEXT_ENCODING_UNKNOWN |
budget_excerpt_v1 | No context entry exceeds the budget; oversized events served as excerpts; the context receipt | none | layers.events[].content._partial_reason: "budget_excerpt", _original_bytes, _excerpt_ranges; context_contributors rows with evicted_from_layers |
capture_language_v1 | Language, script and time-zone hints on captures | language, scripts[], context.timezone on /v1/experience and /bulk | language, scripts, language_source, context.timezone on event reads and recall items |
erasure_blob_cascade_v1 | Erasure deletes the source blob; the raw residual pass | none | source_blob_ids on events; purge on /v1/forget; purge.blobs, purge.retained_shared_blobs, log_residual_cleared_at, log_residual_pending |
matched_fragments_v1 | Which chunks of a long event matched | none | layers.events[].matched_fragments[]; the context_fragments trail entry |
query_variants_v1 | Extra phrasings of the question | query_variants[] on /v1/recall and /v1/recall/stream | per-variant ranks in the diagnostics side table |
recall_diagnostics_v1 | Per-channel recall diagnostics | diagnostics: "full" (existing field) | diagnostics.items, drops, channel_lists, items_truncated |
recall_language_v1 | Language and script hints on recall | language, scripts[] on /v1/recall and /v1/recall/stream | pack warnings[] |
refers_to_v1 | The times a memory talks about | context.refers_to[] on captures; temporal.refers_during on recall and answer | context.refers_to on events |
temporal_lenient_v1 | Lenient time phrases and calendar time zones | temporal.natural_mode, temporal.timezone on recall and answer | the 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:
| Switch | Default | Effect | Pre-fix value |
|---|
CORTEX_EVIDENCE_ROLE_CLASSIFIER | packer | Timestamp- and role-aware evidence classifier for the answer packer and the multi-session preamble (full: every consumer) | legacy |
CORTEX_CONTEXT_STRIP_ECHO | paired | Drop 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_OVERSIZED | on | Skip an event that does not fit and keep packing (affects every ranked /v1/recall) | 0 |
CORTEX_FACET_PACK_POLICY | tiered | In a byte-congested merge of the primary and facet recalls, focused dialogue and fact rows first | legacy |
CORTEX_CONTEXT_ASSISTANT_EXCERPTS | off | Opt-in: long assistant turns render only their matched chunk spans (CORTEX_CONTEXT_ASSISTANT_EXCERPT_MAX_BYTES, default 600) | 0 |
CORTEX_ABSTAIN_SELF_RESCUE | legacy pass | The abstention self-rescue; guarded (opt-in) skips it when the abstention denies the question's premise; 0 never rescues | unset |
CORTEX_RECALL_BUDGET_EXCERPTS | on | New in 0.10.1: budget excerpts, below | 0 |
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):
| Shape | Before | 0.10.1 |
|---|
One 8 KB document, max_tokens: 2000 (7,000-byte budget) | 8,227-byte block, empty events layer | 6,874 bytes; a 6,839-byte excerpt in the layer |
One 32 KB document, max_tokens: 1000 | 32,803 bytes (9.4 times the budget) | 3,493 bytes |
Mixed pack whose 8 KB answer did not fit, max_tokens: 2000 | 188 bytes, answer missing | 6,620 bytes, answer as a 6,390-byte excerpt |
One 9 KB Devanagari document, max_tokens: 2000 | 9,303 bytes | 6,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:
reason | Meaning |
|---|
pass_pending | The pass has not finished the work this job recorded yet |
fulltext_segment_over_budget | A 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_covered | No 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:
| Request | 1M events, without | 1M, 0.10.1 | 5M events, without | 5M, 0.10.1 |
|---|
/v1/forget by id, large scope | 12.2-13.0 s | 0.04-0.37 s | 64.0-66.8 s | 0.09-0.27 s |
| the same forget repeated | 4.4 s | 6 ms | 23.4 s | 90 ms |
/v1/forget, 100 ids | 14.0 s | 0.21 s | 71.2 s | 0.57 s |
/v1/erasures, 1 id | 12.7-13.5 s | 0.05-0.12 s | 67.6-71.1 s | 0.05-0.07 s |
/v1/erasures, 1,000 ids | 25.2 s | 0.64 s | 105.0 s | 2.3 s |
/v1/erasures, 1 blob-backed id | 27.7-27.9 s | 0.06-0.14 s | 137.5-140.1 s | 0.07-0.11 s |
/v1/forget scope-wide, small scope | 21.8-22.0 s | 0.44-0.47 s | 103.5-179.5 s | 2.5-2.6 s |
/v1/erasures whole scope, small scope | 33.3 s | 0.56 s | 175.3 s | 2.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):
| Route | Access | Purpose |
|---|
POST /v1/admin/derived/rederive | trusted operator, admin.compact | Plan (dry run, default) or run a re-derivation job |
GET /v1/admin/derived/rederive/{job_id} | trusted operator, diagnostics.read | Read a re-derivation job |
POST /v1/admin/derived/resolver/unmerge | trusted operator, admin.compact | Report (dry run, default) or split names the resolver merged |
GET /v1/admin/events/{id}/chunks | trusted operator, diagnostics.read | How one event was chunked, with fragment offsets |
POST /v1/admin/index-audit/source-blob-links | trusted 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-blobs | trusted operator, diagnostics.read | Live 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.
| Variable | Default | What it does |
|---|
CORTEX_RECALL_BUDGET_EXCERPTS | on | Budget excerpts and the strict budget (0, off or false restores the pre-0.10.1 render) |
CORTEX_EVIDENCE_ROLE_CLASSIFIER | packer | Answer-context evidence classifier (full, legacy) |
CORTEX_CONTEXT_STRIP_ECHO | paired | Echo strip (all, 0) |
CORTEX_CONTEXT_RENDER_SKIP_OVERSIZED | on | Skip an oversized event and keep packing |
CORTEX_FACET_PACK_POLICY | tiered | Facet merge policy (legacy; byte_share, tiered_byte_share, tiered_facts_late for evaluation) |
CORTEX_CONTEXT_ASSISTANT_EXCERPTS | off | Opt-in assistant-turn excerpts (1, ms) |
CORTEX_CONTEXT_ASSISTANT_EXCERPT_MAX_BYTES | 600 | Per-event cap of those excerpts |
CORTEX_QUERY_VARIANTS_FUSION | interleave | Variant fusion (evaluation: depth_cap, deep_rrf) |
CORTEX_QUERY_VARIANTS_DEPTH | 10 | Depth for depth_cap (evaluation) |
CORTEX_EMBEDDING_CACHE_DISABLE | unset | Measurement only: bypass the provider embedding cache |
CORTEX_CHUNK_FRAGMENTS | on | Write chunk fragment rows |
CORTEX_ENRICHMENT_SKIP_NON_EN | off | Skip LLM enrichment for non-Latin-script text |
CORTEX_POLARITY_RECHECK | on | The polarity re-check call (0 records the doubt without a call) |
CORTEX_POLARITY_QUOTE_TRIGGER_EN | on | The English quote trigger of that re-check |
CORTEX_ENRICHMENT_SCOPE_QUIET_MS | 0 (off) | Hold a busy scope's captures for one batched extraction (at most 60000) |
CORTEX_ENRICHMENT_SCOPE_QUIET_MAX_MS | 30000 | Longest hold of that window |
CORTEX_DESTRUCTIVE_LEASE_WAIT_SECS | 120 | How long a capture waits for a forget's or erasure's lease (1 to 3600) |
CORTEX_SHUTDOWN_DESTRUCTIVE_WAIT_SECS | 60 | How long a graceful stop waits for running forgets and erasures (0 to 3600) |
CORTEX_ERASURE_RESIDUAL | on | The raw residual pass (0 off; a startup warning says so) |
CORTEX_ERASURE_RESIDUAL_SHUTDOWN_SECS | 20 | Residual drain at a graceful stop |
CORTEX_ERASURE_RESIDUAL_MAX_BYTES | 536870912 | SST bytes one pass may rewrite per store |
CORTEX_ERASURE_RESIDUAL_FULLTEXT_MAX_DOCS | 1000000 | Largest full-text segment one pass merges |
CORTEX_ERASURE_RESIDUAL_MAX_PENDING_KEYS | 131072 | Recorded keys waiting per store |
CORTEX_ERASURE_RESIDUAL_MIN_INTERVAL_MS | 30000 | Minimum gap between passes |
CORTEX_ERASURE_RESIDUAL_DEBOUNCE_MS | 1000 | Quiet time before a pass starts |
CORTEX_ERASURE_RESIDUAL_PARKED_RECHECK_SECS | 900 | Recheck interval of a parked over-budget segment |
CORTEX_ERASURE_PERIODIC_COMPACTION_SECS | unset | Periodic compaction backstop for the stores (for example 86400) |
CORTEX_ERASURE_INDEX | on | Read the erasure index (0: maintained, never read) |
CORTEX_ERASURE_INDEX_BACKFILL | on | Backfill the index on a store written before it |
CORTEX_ERASURE_INDEX_BACKFILL_ROWS | 20000 | Rows per backfill step |
CORTEX_ERASURE_INDEX_BACKFILL_PAUSE_MS | 20 | Pause between backfill steps |
CORTEX_CODE_REPO_ROOTS | unset (startup warning) | Directories code-plane repositories may be registered from |
CORTEX_EVAL_FACET_PLAN_OVERRIDE | off | Evaluation only; never in production |
CORTEX_EVAL_DUMP_ANSWER_POOLS | off | Evaluation 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:
| Status | Code | Where | Retriable |
|---|
| 422 | INVALID_BODY | A 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 |
| 422 | INVALID_ENVELOPE | A malformed capture language, scripts, context.timezone or context.refers_to | no |
| 422 | BLOB_TEXT_ENCODING_UNKNOWN | A capture referencing a text blob whose bytes are not valid in any declared or detectable encoding | no |
| 409 | DESTRUCTIVE_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 one | yes (Retry-After: 5) |
| 409 | SCOPE_STATE_CHANGED | /v1/scopes register, member edit and delete inside that footprint; a capture whose lease wait ended just as the lease was released | yes |
| 503 | SERVER_SHUTTING_DOWN | /v1/forget, /v1/erasures after a graceful stop began; nothing was changed | yes (Retry-After: 5, against the restarted server) |
| 409 | REDERIVE_IN_PROGRESS | A forget, an erasure, their previews or a second re-derivation on a scope root a re-derivation job holds | yes (Retry-After: 30) |
| 409 | ENRICHMENT_OFF | POST /v1/admin/derived/rederive with enrichment off | no |
| 409 | JOB_MISMATCH | A re-derivation job_id reused for other targets, actions or billing tenant | no |
| 409 | JOB_NOT_COMPLETED | continue_from names a job that has not completed | no |
| 409 | JOB_ALREADY_CONTINUED | continue_from names a job another job already continued (details.continued_by) | no |
| 404 | JOB_NOT_FOUND | continue_from names an unknown or foreign job | no |
| 422 | COLD_BACKUP_ACK_REQUIRED | A re-derivation apply with purge_derived and no cold_backup_ack: true | no |
| 422 | NO_ELIGIBLE_EVENTS | A re-derivation apply that would purge or re-enrich, with no eligible event; nothing changed | no |
| 422 | NO_TARGETS | A re-derivation or unmerge request that names nothing to repair, or a continue_from job that left nothing | no |
| 422 | INVALID_AUDIT | A re-derivation or unmerge audit report that cannot be read | no |
| 503 | REDERIVE_UNSUPPORTED | The coordinator cannot run re-derivation | no |
| 500 | REDERIVE_FAILED, REDERIVE_PLAN_FAILED, UNMERGE_FAILED | Planning 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 |
| 409 | UNIFIED_WAL_REQUIRED | source-blob-links on the legacy dual WAL | no |
| 500 | SOURCE_BLOB_LINKS_FAILED, ORPHAN_BLOB_REPORT_FAILED, CHUNK_INSPECTION_FAILED | Failures of the new operator routes | yes |
| 422 | EVAL_OVERRIDE_DISABLED | /v1/answer with facet_plan_override on a server without the evaluation flag | no |
| 403 | SCOPE_REGISTRATION_REQUIRED | Code plane: registering a repository directly under a root scope | no |
| 409 | REPO_SCOPE_IN_USE | Code plane: the binding another repository already holds | no |
| 422 | REPO_PATH_NOT_ALLOWED | Code plane: a path outside CORTEX_CODE_REPO_ROOTS or always refused (details.reason) | no |
| 400 | INVALID_PATHS | Code plane: a malformed certify paths filter | no |
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
- 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.
- 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.
- 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.
- 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.
| Package | Registry | Previous version | Released with 0.10.1 | What is new |
|---|
cortexdbai (Python) | PyPI | 0.11.2 | 0.12.0 | version(), 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) | npm | 0.9.3 | 0.10.0 | The 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-mcp | PyPI | 0.7.5 | 0.8.0 | Matched document spans in memory_search output; clearer code-plane tool output |
cortexdb-cli | PyPI | 0.5.4 | 0.6.0 | cortexdb auth tokens --scope is now where your auth.mint is checked, and --confine CAP:PREFIX carries the token's scopes |
cortexdb-connectors | PyPI | 0.2.21 | 0.2.22 | GitHub: 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.